├── env.d ├── terraform │ └── .gitkeep └── development │ ├── crowdin.dist │ ├── postgresql │ └── localtunnel.dist ├── src ├── backend │ ├── joanie │ │ ├── __init__.py │ │ ├── badges │ │ │ ├── __init__.py │ │ │ ├── migrations │ │ │ │ └── __init__.py │ │ │ ├── views.py │ │ │ ├── exceptions.py │ │ │ ├── apps.py │ │ │ └── admin.py │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── fields │ │ │ │ └── __init__.py │ │ │ ├── management │ │ │ │ ├── __init__.py │ │ │ │ └── commands │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── synchronize_offerings.py │ │ │ │ │ └── synchronize_brevo_subscriptions.py │ │ │ ├── migrations │ │ │ │ ├── __init__.py │ │ │ │ ├── 0074_alter_quote_options.py │ │ │ │ ├── 0045_contractdefinition_appendix.py │ │ │ │ ├── 0012_producttranslation_instructions.py │ │ │ │ ├── 0029_user_has_subscribed_to_commercial_newsletter.py │ │ │ │ ├── 0065_ordergroup_description.py │ │ │ │ ├── 0076_quote_reference.py │ │ │ │ ├── 0005_courseproductrelation_add_max_validated_orders.py │ │ │ │ ├── 0031_alter_order_state.py │ │ │ │ ├── 0077_batchorder_payment_method.py │ │ │ │ ├── 0046_order_has_waived_withdrawal_right.py │ │ │ │ ├── 0036_order_state_migration.py │ │ │ │ ├── 0055_alter_documentimage_options_alter_ordergroup_options.py │ │ │ │ ├── 0044_alter_certificatedefinition_template.py │ │ │ │ ├── 0073_product_quote_definition.py │ │ │ │ ├── 0083_remove_batchorder_voucher_remove_order_voucher_and_more.py │ │ │ │ ├── 0079_alter_batchorder_trainees.py │ │ │ │ ├── 0030_order_payment_schedule.py │ │ │ │ ├── 0035_order_credit_card.py │ │ │ │ ├── 0033_alter_order_state.py │ │ │ │ ├── 0040_alter_order_payment_schedule.py │ │ │ │ ├── 0087_remove_batchorder_contract_contract_batch_order.py │ │ │ │ ├── 0017_alter_ordergroup_course_product_relation.py │ │ │ │ ├── 0048_alter_order_payment_schedule.py │ │ │ │ ├── 0069_remove_batchorder_offering_rules_and_more.py │ │ │ │ ├── 0038_alter_order_state.py │ │ │ │ ├── 0088_alter_batchorder_state.py │ │ │ │ ├── 0026_course_effort_user_phone_number.py │ │ │ │ ├── 0042_alter_order_state.py │ │ │ │ ├── 0057_alter_ordergroup_options_ordergroup_discount.py │ │ │ │ ├── 0085_alter_quotedefinitiontranslation_unique_together_and_more.py │ │ │ │ ├── 0039_alter_order_has_consent_to_terms_and_more.py │ │ │ │ ├── 0043_address_unique_address_per_user_and_more.py │ │ │ │ ├── 0037_alter_order_state.py │ │ │ │ ├── 0004_course_cover_alter_organization_logo.py │ │ │ │ ├── 0081_alter_contractdefinition_name.py │ │ │ │ ├── 0051_remove_deprecated_choice_contractdefinition_name.py │ │ │ │ ├── 0034_alter_order_state.py │ │ │ │ └── 0053_alter_certificate_options_and_more.py │ │ │ ├── templatetags │ │ │ │ └── __init__.py │ │ │ ├── context_processors │ │ │ │ ├── __init__.py │ │ │ │ ├── admin.py │ │ │ │ └── contract_definition.py │ │ │ ├── templates │ │ │ │ ├── issuers │ │ │ │ │ ├── contract_definition_unicamp.css │ │ │ │ │ ├── professional_training_agreement_unicamp.css │ │ │ │ │ ├── contract_definition_unicamp.html │ │ │ │ │ ├── professional_training_agreement_unicamp.html │ │ │ │ │ └── microcredential_degree_unicamp.html │ │ │ │ ├── contract_definition │ │ │ │ │ ├── fragment_logo.html │ │ │ │ │ └── fragment_course_plan.html │ │ │ │ ├── certificate │ │ │ │ │ └── fragment_logo.html │ │ │ │ ├── admin │ │ │ │ │ └── base.html │ │ │ │ └── debug │ │ │ │ │ ├── sentry_decrypt.html │ │ │ │ │ └── pdf_viewer.html │ │ │ ├── static │ │ │ │ └── joanie │ │ │ │ │ ├── images │ │ │ │ │ ├── logo_fun.png │ │ │ │ │ ├── logo_unicamp.png │ │ │ │ │ ├── degree_bg_frame.png │ │ │ │ │ ├── degree_bg_decorator.png │ │ │ │ │ ├── degree_fun_verified_badge.png │ │ │ │ │ ├── default_degree_bg_decorator_en.png │ │ │ │ │ ├── default_degree_bg_decorator_fr.png │ │ │ │ │ ├── unicamp_degree_bg_decorator_en.png │ │ │ │ │ ├── unicamp_degree_bg_decorator_fr.png │ │ │ │ │ └── flag_europe.svg │ │ │ │ │ └── fonts │ │ │ │ │ └── barlow │ │ │ │ │ ├── 400.woff2 │ │ │ │ │ ├── 600.woff2 │ │ │ │ │ ├── 700.woff2 │ │ │ │ │ ├── 800.woff2 │ │ │ │ │ └── 400-italic.woff2 │ │ │ ├── filters │ │ │ │ ├── __init__.py │ │ │ │ └── admin │ │ │ │ │ └── skill.py │ │ │ ├── views │ │ │ │ ├── __init__.py │ │ │ │ └── redirect.py │ │ │ ├── flows │ │ │ │ └── __init__.py │ │ │ ├── serializers │ │ │ │ └── __init__.py │ │ │ ├── utils │ │ │ │ ├── course_run │ │ │ │ │ └── __init__.py │ │ │ │ ├── newsletter │ │ │ │ │ └── __init__.py │ │ │ │ ├── discount.py │ │ │ │ ├── billing_address.py │ │ │ │ └── product.py │ │ │ ├── pagination.py │ │ │ ├── models │ │ │ │ └── __init__.py │ │ │ └── storages.py │ │ ├── debug │ │ │ └── __init__.py │ │ ├── demo │ │ │ ├── __init__.py │ │ │ ├── management │ │ │ │ ├── __init__.py │ │ │ │ └── commands │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── create_dev_demo.py │ │ │ └── defaults.py │ │ ├── edx_imports │ │ │ ├── __init__.py │ │ │ ├── management │ │ │ │ ├── __init__.py │ │ │ │ └── commands │ │ │ │ │ └── __init__.py │ │ │ ├── urls.py │ │ │ └── tasks │ │ │ │ └── __init__.py │ │ ├── signature │ │ │ ├── __init__.py │ │ │ ├── management │ │ │ │ ├── __init__.py │ │ │ │ └── commands │ │ │ │ │ └── __init__.py │ │ │ ├── backends │ │ │ │ └── __init__.py │ │ │ ├── urls.py │ │ │ └── api.py │ │ ├── tests │ │ │ ├── badges │ │ │ │ └── __init__.py │ │ │ ├── core │ │ │ │ ├── __init__.py │ │ │ │ ├── admin │ │ │ │ │ └── __init__.py │ │ │ │ ├── api │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── admin │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── orders │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ └── test_create.py │ │ │ │ │ │ ├── batch_order │ │ │ │ │ │ │ └── __init__.py │ │ │ │ │ │ ├── offerings │ │ │ │ │ │ │ └── __init__.py │ │ │ │ │ │ ├── products │ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ │ └── test_delete.py │ │ │ │ │ │ └── vouchers │ │ │ │ │ │ │ └── __init__.py │ │ │ │ │ ├── order │ │ │ │ │ │ └── __init__.py │ │ │ │ │ ├── quotes │ │ │ │ │ │ └── __init__.py │ │ │ │ │ ├── activity_log │ │ │ │ │ │ └── __init__.py │ │ │ │ │ ├── batch_order │ │ │ │ │ │ └── __init__.py │ │ │ │ │ ├── organizations │ │ │ │ │ │ └── __init__.py │ │ │ │ │ └── remote_endpoints │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── course_run │ │ │ │ │ │ └── __init__.py │ │ │ │ │ │ └── newsletter │ │ │ │ │ │ └── __init__.py │ │ │ │ ├── debug │ │ │ │ │ └── __init__.py │ │ │ │ ├── models │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── order │ │ │ │ │ │ └── __init__.py │ │ │ │ ├── tasks │ │ │ │ │ └── __init__.py │ │ │ │ ├── utils │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── newsletter │ │ │ │ │ │ └── __init__.py │ │ │ │ │ ├── __diff__ │ │ │ │ │ │ ├── microcredential_degree_default_original.png │ │ │ │ │ │ └── microcredential_degree_unicamp_original.png │ │ │ │ │ └── test_file_checksum.py │ │ │ │ ├── test_models_contract.perf.yml │ │ │ │ ├── test_models_course_access.perf.yml │ │ │ │ ├── test_models_organization.perf.yml │ │ │ │ ├── test_models_organization_access.perf.yml │ │ │ │ ├── test_api_address.perf.yml │ │ │ │ ├── test_utils.py │ │ │ │ └── test_api_course_run.perf.yml │ │ │ ├── demo │ │ │ │ └── __init__.py │ │ │ ├── edx_imports │ │ │ │ ├── __init__.py │ │ │ │ └── images │ │ │ │ │ └── creative_common.jpeg │ │ │ ├── lms_handler │ │ │ │ └── __init__.py │ │ │ ├── payment │ │ │ │ ├── __init__.py │ │ │ │ └── lyra │ │ │ │ │ └── responses │ │ │ │ │ ├── cancel_token.json │ │ │ │ │ ├── cancel_and_refund_failed.json │ │ │ │ │ ├── is_already_paid_failed.json │ │ │ │ │ └── create_payment_failed.json │ │ │ ├── signature │ │ │ │ ├── __init__.py │ │ │ │ └── backends │ │ │ │ │ └── __init__.py │ │ │ ├── swagger │ │ │ │ └── __init__.py │ │ │ ├── static │ │ │ │ └── joanie │ │ │ │ │ └── red-square.webp │ │ │ └── __init__.py │ │ ├── payment │ │ │ ├── backends │ │ │ │ └── __init__.py │ │ │ ├── migrations │ │ │ │ ├── __init__.py │ │ │ │ ├── 0002_alter_creditcard_options.py │ │ │ │ ├── 0011_remove_creditcard_is_main_remove_creditcard_owner.py │ │ │ │ ├── 0008_creditcard_initial_issuer_transaction_identifier.py │ │ │ │ ├── 0007_alter_invoice_localized_context.py │ │ │ │ └── 0004_rename_recipient_address_invoice_deprecated_recipient_address_and_more.py │ │ │ ├── urls.py │ │ │ ├── apps.py │ │ │ └── enums.py │ │ ├── lms_handler │ │ │ ├── backends │ │ │ │ └── __init__.py │ │ │ └── urls.py │ │ ├── wsgi.py │ │ ├── remote_endpoints_urls.py │ │ └── celery_app.py │ ├── locale │ │ ├── es_ES │ │ │ └── LC_MESSAGES │ │ │ │ └── django.mo │ │ ├── fr_CA │ │ │ └── LC_MESSAGES │ │ │ │ └── django.mo │ │ └── fr_FR │ │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── MANIFEST.in │ ├── setup.py │ └── manage.py ├── tray │ ├── tray.yml │ ├── templates │ │ └── services │ │ │ ├── app │ │ │ ├── configs │ │ │ │ ├── __init__.py.j2 │ │ │ │ └── settings.py.j2 │ │ │ ├── _env.yml.j2 │ │ │ ├── deploy_celery.yml.j2 │ │ │ ├── secret.yml.j2 │ │ │ ├── svc.yml.j2 │ │ │ └── deploy_app.yml.j2 │ │ │ ├── nginx │ │ │ ├── configs │ │ │ │ └── healthcheck.conf.j2 │ │ │ ├── secret.yml.j2 │ │ │ ├── svc.yml.j2 │ │ │ └── static-svc.yml.j2 │ │ │ ├── admin │ │ │ ├── configs │ │ │ │ └── healthcheck.conf.j2 │ │ │ ├── svc.yml.j2 │ │ │ └── static-svc.yml.j2 │ │ │ └── postgresql │ │ │ ├── secret.yml.j2 │ │ │ └── ep.yml.j2 │ └── vars │ │ └── settings.yml ├── frontend │ └── admin │ │ ├── src │ │ ├── utils │ │ │ ├── index.tsx │ │ │ ├── testing.ts │ │ │ ├── createEmotionCache.ts │ │ │ ├── string.ts │ │ │ ├── numbers.ts │ │ │ ├── array.ts │ │ │ ├── dates.tsx │ │ │ ├── constants.ts │ │ │ ├── arrayUtils.spec.tsx │ │ │ └── filters.spec.tsx │ │ ├── styles │ │ │ ├── abstracts │ │ │ │ ├── _variables.scss │ │ │ │ ├── _index.scss │ │ │ │ └── _rem.scss │ │ │ └── globals.scss │ │ ├── services │ │ │ ├── api │ │ │ │ └── models │ │ │ │ │ ├── Ressource.ts │ │ │ │ │ ├── User.ts │ │ │ │ │ ├── Skill.ts │ │ │ │ │ ├── GeneratedCertificate.ts │ │ │ │ │ ├── Teacher.ts │ │ │ │ │ ├── Image.ts │ │ │ │ │ ├── Accesses.ts │ │ │ │ │ ├── QuoteDefinition.ts │ │ │ │ │ ├── CertificateDefinition.ts │ │ │ │ │ ├── CourseRun.ts │ │ │ │ │ ├── Discount.ts │ │ │ │ │ ├── Voucher.ts │ │ │ │ │ ├── ContractDefinition.ts │ │ │ │ │ └── OfferingRule.ts │ │ │ ├── http │ │ │ │ └── HttpError.ts │ │ │ ├── repositories │ │ │ │ ├── auth │ │ │ │ │ └── AuthRepository.ts │ │ │ │ └── AbstractRepository.ts │ │ │ └── factories │ │ │ │ ├── skill │ │ │ │ └── index.ts │ │ │ │ ├── teacher │ │ │ │ └── index.ts │ │ │ │ ├── users │ │ │ │ └── index.ts │ │ │ │ ├── images │ │ │ │ └── index.ts │ │ │ │ ├── discounts │ │ │ │ └── index.ts │ │ │ │ ├── certificate-definition │ │ │ │ └── index.ts │ │ │ │ ├── quote-definition │ │ │ │ └── index.ts │ │ │ │ ├── accesses │ │ │ │ └── index.ts │ │ │ │ ├── voucher │ │ │ │ └── index.ts │ │ │ │ └── contract-definition │ │ │ │ └── index.ts │ │ ├── types │ │ │ ├── i18n │ │ │ │ └── LocalesEnum.ts │ │ │ ├── api.ts │ │ │ ├── routes.ts │ │ │ ├── auth.ts │ │ │ └── utils.ts │ │ ├── components │ │ │ ├── presentational │ │ │ │ ├── breadrumbs │ │ │ │ │ ├── type.ts │ │ │ │ │ └── CustomBreadcrumbs.spec.tsx │ │ │ │ ├── menu-popover │ │ │ │ │ └── types.ts │ │ │ │ ├── card │ │ │ │ │ ├── SimpleCard.spec.tsx │ │ │ │ │ └── SimpleCard.tsx │ │ │ │ ├── button │ │ │ │ │ └── CloseNotificationButton.tsx │ │ │ │ ├── tooltip │ │ │ │ │ └── ConditionalTooltip.tsx │ │ │ │ ├── link │ │ │ │ │ └── CustomLink.tsx │ │ │ │ ├── loading │ │ │ │ │ └── LoadingContent.tsx │ │ │ │ ├── modal │ │ │ │ │ └── useModal.tsx │ │ │ │ ├── dnd │ │ │ │ │ ├── StyledDndItemContainer.tsx │ │ │ │ │ └── StrictModeDroppable.tsx │ │ │ │ ├── credit-card-brand-logo │ │ │ │ │ ├── CreditCardBrandLogo.tsx │ │ │ │ │ └── CreditCardBrandLogo.spec.tsx │ │ │ │ ├── hook-form │ │ │ │ │ └── RHFTextField.tsx │ │ │ │ └── files │ │ │ │ │ └── thumbnail │ │ │ │ │ └── FileThumbnail.spec.tsx │ │ │ ├── templates │ │ │ │ ├── courses │ │ │ │ │ └── form │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── sections │ │ │ │ │ │ └── offering │ │ │ │ │ │ └── OfferingsSection.tsx │ │ │ │ ├── products │ │ │ │ │ └── form │ │ │ │ │ │ └── sections │ │ │ │ │ │ └── offerings │ │ │ │ │ │ └── ProductFormOfferings.tsx │ │ │ │ ├── form │ │ │ │ │ └── buttons │ │ │ │ │ │ └── UseAsTemplateButton.tsx │ │ │ │ └── certificates-definitions │ │ │ │ │ └── modals │ │ │ │ │ └── CreateOrEditCertificationModal.spec.tsx │ │ │ └── testing │ │ │ │ ├── utils.ts │ │ │ │ └── PlaywrightCustomRouter.tsx │ │ ├── layouts │ │ │ └── dashboard │ │ │ │ ├── nav │ │ │ │ └── item │ │ │ │ │ ├── DashboardNavItem.module.scss │ │ │ │ │ └── list │ │ │ │ │ ├── DashboardNavItemsList.module.scss │ │ │ │ │ └── DasboardNavItemsList.tsx │ │ │ │ └── header │ │ │ │ ├── DashboardLayoutHeader.module.scss │ │ │ │ └── actions │ │ │ │ └── DashboardLayoutHeaderActions.tsx │ │ ├── hooks │ │ │ ├── form │ │ │ │ ├── useFormSubmit.ts │ │ │ │ └── useFormSubmit.spec.ts │ │ │ ├── useFromIdSearchParams.ts │ │ │ ├── usePrevious.ts │ │ │ ├── useAllLanguages │ │ │ │ └── useAllLanguages.tsx │ │ │ └── useCopyToClipboard.tsx │ │ ├── tests │ │ │ ├── orders │ │ │ │ └── OrderListItemTestScenario.ts │ │ │ ├── batch-orders │ │ │ │ ├── BatchOrderListItemTestScenario.ts │ │ │ │ └── BatchOrderTestScenario.ts │ │ │ ├── vouchers │ │ │ │ └── VouchersTestScenario.ts │ │ │ ├── mocks │ │ │ │ ├── certificate-definitions │ │ │ │ │ └── certificate-definition-mocks.ts │ │ │ │ ├── quote-definitions │ │ │ │ │ └── quote-definition-mocks.ts │ │ │ │ ├── course-runs │ │ │ │ │ └── course-runs-mocks.ts │ │ │ │ └── courses │ │ │ │ │ └── course-mocks.ts │ │ │ └── useResourceHandler.spec.tsx │ │ ├── theme │ │ │ ├── types.ts │ │ │ └── JoanieThemeProvider.tsx │ │ ├── pages │ │ │ ├── index.tsx │ │ │ ├── _document.tsx │ │ │ └── admin │ │ │ │ ├── orders │ │ │ │ └── index.tsx │ │ │ │ ├── courses │ │ │ │ └── index.tsx │ │ │ │ ├── products │ │ │ │ └── index.tsx │ │ │ │ ├── vouchers │ │ │ │ └── index.tsx │ │ │ │ ├── courses-runs │ │ │ │ └── index.tsx │ │ │ │ ├── enrollments │ │ │ │ └── index.tsx │ │ │ │ ├── organizations │ │ │ │ └── index.tsx │ │ │ │ ├── certificates-definitions │ │ │ │ └── index.tsx │ │ │ │ ├── quotes-definitions │ │ │ │ └── index.tsx │ │ │ │ └── contracts-definitions │ │ │ │ └── index.tsx │ │ ├── translations │ │ │ ├── common │ │ │ │ ├── languageTranslations.ts │ │ │ │ └── entitiesInputLabel.ts │ │ │ ├── enrollments │ │ │ │ └── enrollment-state.ts │ │ │ ├── pages │ │ │ │ ├── batch-orders │ │ │ │ │ └── breadcrumbsTranslations.ts │ │ │ │ ├── orders │ │ │ │ │ └── breadcrumbsTranslations.ts │ │ │ │ └── enrollments │ │ │ │ │ └── breadcrumbsTranslations.ts │ │ │ └── products │ │ │ │ └── types.ts │ │ └── contexts │ │ │ ├── auth │ │ │ └── AuthContext.tsx │ │ │ └── i18n │ │ │ └── TranslationsProvider │ │ │ └── TranslationContext.tsx │ │ ├── public │ │ └── favicon.ico │ │ ├── mocks │ │ ├── server.ts │ │ ├── browser.ts │ │ ├── index.ts │ │ └── handlers │ │ │ ├── users │ │ │ └── index.ts │ │ │ ├── contract-definitions │ │ │ └── index.ts │ │ │ ├── certificate-definitions │ │ │ └── index.ts │ │ │ ├── auth │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── jest.polyfills.js │ │ ├── .gitignore │ │ ├── next-env.d.ts │ │ ├── jest.env.js │ │ ├── playwright │ │ └── index.html │ │ ├── .env.example │ │ ├── tsconfig.json │ │ └── next.config.js ├── terraform │ ├── providers.tf │ ├── create_state_bucket │ │ ├── provider.tf │ │ ├── state.tf │ │ └── swift.tf │ ├── variables.tf │ ├── output.tf │ └── state.tf ├── mail │ ├── html-to-text.config.json │ ├── bin │ │ ├── mjml-to-html │ │ └── html-to-plain-text │ ├── mjml │ │ └── partial │ │ │ ├── footer.mjml │ │ │ └── welcome.mjml │ └── package.json └── openApiClientJs │ ├── scripts │ └── openapi-typescript-codegen │ │ └── generate_api_client_local.sh │ └── package.json ├── arnold.yml ├── docs ├── assets │ ├── lex_persona_schema_api.png │ ├── lex_persona_schema_user.png │ ├── moodle_add_new_user_1.png │ ├── moodle_add_new_user_2.png │ ├── moodle_assign_system_role_1.png │ ├── moodle_assign_system_role_2.png │ ├── moodle_assign_system_role_3.png │ ├── moodle_add_external_webservice_1.png │ ├── moodle_add_external_webservice_2.png │ ├── moodle_add_external_webservice_3.png │ ├── moodle_add_external_webservice_functions_1.png │ ├── moodle_add_external_webservice_functions_2.png │ ├── moodle_add_external_webservice_authorized_users_1.png │ └── moodle_add_external_webservice_authorized_users_2.png ├── explanation │ └── api-client-typescript.md └── reference │ └── workflows.md ├── bin ├── manage ├── compose ├── pytest ├── sqlacodegen ├── update_openapi_schema ├── get_tunnel_url ├── state └── terraform ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Support_question.md ├── docker ├── files │ ├── usr │ │ └── local │ │ │ └── etc │ │ │ └── gunicorn │ │ │ └── joanie.py │ ├── etc │ │ └── nginx │ │ │ └── conf.d │ │ │ └── default.conf │ └── admin │ │ └── etc │ │ └── nginx │ │ └── conf.d │ │ └── default.conf └── images │ └── admin │ └── Dockerfile ├── .dockerignore ├── crowdin └── config.yml └── UPGRADE.md /env.d/terraform/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/badges/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/debug/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/core/fields/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/edx_imports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/signature/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/badges/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/badges/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/core/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/core/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/demo/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/payment/backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/admin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/debug/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/edx_imports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/lms_handler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/payment/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/signature/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/swagger/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/edx_imports/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/lms_handler/backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/payment/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/signature/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/admin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/order/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/quotes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/core/context_processors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/core/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/demo/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/activity_log/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/admin/orders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/batch_order/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/models/order/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/utils/newsletter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/edx_imports/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/signature/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/admin/batch_order/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/admin/offerings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/admin/products/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/admin/vouchers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/organizations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/remote_endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/badges/views.py: -------------------------------------------------------------------------------- 1 | """Views for Joanie badges app.""" 2 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/remote_endpoints/course_run/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/api/remote_endpoints/newsletter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tray/tray.yml: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: joanie 3 | version: 3.1.2 4 | -------------------------------------------------------------------------------- /src/frontend/admin/src/utils/index.tsx: -------------------------------------------------------------------------------- 1 | export const noop = () => undefined; 2 | -------------------------------------------------------------------------------- /src/terraform/providers.tf: -------------------------------------------------------------------------------- 1 | provider "openstack" { 2 | alias = "ovh" 3 | } 4 | -------------------------------------------------------------------------------- /src/tray/templates/services/app/configs/__init__.py.j2: -------------------------------------------------------------------------------- 1 | """custom settings.""" 2 | -------------------------------------------------------------------------------- /src/tray/templates/services/app/configs/settings.py.j2: -------------------------------------------------------------------------------- 1 | from ..settings import * 2 | -------------------------------------------------------------------------------- /src/frontend/admin/src/styles/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | $navigationWidth: 280px; 2 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/signature/backends/__init__.py: -------------------------------------------------------------------------------- 1 | """Backend signature tests.""" 2 | -------------------------------------------------------------------------------- /src/tray/vars/settings.yml: -------------------------------------------------------------------------------- 1 | databases: 2 | - engine: "postgresql" 3 | release: "9.6" 4 | -------------------------------------------------------------------------------- /src/frontend/admin/src/styles/abstracts/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "./variables"; 2 | @forward "./rem"; 3 | -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/issuers/contract_definition_unicamp.css: -------------------------------------------------------------------------------- 1 | contract_definition_default.css -------------------------------------------------------------------------------- /src/tray/templates/services/app/_env.yml.j2: -------------------------------------------------------------------------------- 1 | DJANGO_SILENCED_SYSTEM_CHECKS: security.W008,security.W004 2 | -------------------------------------------------------------------------------- /arnold.yml: -------------------------------------------------------------------------------- 1 | # arnold.yml 2 | metadata: 3 | name: joanie 4 | version: 3.1.2 5 | source: 6 | path: src/tray 7 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/Ressource.ts: -------------------------------------------------------------------------------- 1 | export interface ResourceWithId { 2 | id: string; 3 | } 4 | -------------------------------------------------------------------------------- /docs/assets/lex_persona_schema_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/lex_persona_schema_api.png -------------------------------------------------------------------------------- /docs/assets/lex_persona_schema_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/lex_persona_schema_user.png -------------------------------------------------------------------------------- /docs/assets/moodle_add_new_user_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_add_new_user_1.png -------------------------------------------------------------------------------- /docs/assets/moodle_add_new_user_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_add_new_user_2.png -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/issuers/professional_training_agreement_unicamp.css: -------------------------------------------------------------------------------- 1 | professional_training_agreement_default.css -------------------------------------------------------------------------------- /src/frontend/admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/frontend/admin/public/favicon.ico -------------------------------------------------------------------------------- /src/frontend/admin/src/types/i18n/LocalesEnum.ts: -------------------------------------------------------------------------------- 1 | export enum LocalesEnum { 2 | FRENCH = "fr-fr", 3 | ENGLISH = "en-us", 4 | } 5 | -------------------------------------------------------------------------------- /docs/assets/moodle_assign_system_role_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_assign_system_role_1.png -------------------------------------------------------------------------------- /docs/assets/moodle_assign_system_role_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_assign_system_role_2.png -------------------------------------------------------------------------------- /docs/assets/moodle_assign_system_role_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_assign_system_role_3.png -------------------------------------------------------------------------------- /env.d/development/crowdin.dist: -------------------------------------------------------------------------------- 1 | CROWDIN_API_TOKEN=Your-Api-Token 2 | CROWDIN_PROJECT_ID=Your-Project-Id 3 | CROWDIN_BASE_PATH=/app/src 4 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/test_models_contract.perf.yml: -------------------------------------------------------------------------------- 1 | ContractModelTestCase.test_models_contract_get_abilities_preset_role: [] 2 | -------------------------------------------------------------------------------- /src/backend/locale/es_ES/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/locale/es_ES/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /src/backend/locale/fr_CA/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/locale/fr_CA/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /src/backend/locale/fr_FR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/locale/fr_FR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /docs/assets/moodle_add_external_webservice_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_add_external_webservice_1.png -------------------------------------------------------------------------------- /docs/assets/moodle_add_external_webservice_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_add_external_webservice_2.png -------------------------------------------------------------------------------- /docs/assets/moodle_add_external_webservice_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_add_external_webservice_3.png -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/contract_definition/fragment_logo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/static/joanie/red-square.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/tests/static/joanie/red-square.webp -------------------------------------------------------------------------------- /bin/manage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck source=bin/_config.sh 4 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh" 5 | 6 | _django_manage "$@" 7 | -------------------------------------------------------------------------------- /bin/compose: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck source=bin/_config.sh 4 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh" 5 | 6 | _docker_compose "$@" 7 | -------------------------------------------------------------------------------- /docs/assets/moodle_add_external_webservice_functions_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_add_external_webservice_functions_1.png -------------------------------------------------------------------------------- /docs/assets/moodle_add_external_webservice_functions_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_add_external_webservice_functions_2.png -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/images/logo_fun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/images/logo_fun.png -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/certificate/fragment_logo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/fonts/barlow/400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/fonts/barlow/400.woff2 -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/fonts/barlow/600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/fonts/barlow/600.woff2 -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/fonts/barlow/700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/fonts/barlow/700.woff2 -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/fonts/barlow/800.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/fonts/barlow/800.woff2 -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/images/logo_unicamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/images/logo_unicamp.png -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/User.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string; 3 | username: string; 4 | full_name: string; 5 | email: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/terraform/create_state_bucket/provider.tf: -------------------------------------------------------------------------------- 1 | 2 | # This provider is configured with the OS_* environment variables 3 | provider "openstack" { 4 | alias = "ovh" 5 | } 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | Description... 4 | 5 | 6 | ## Proposal 7 | 8 | Description... 9 | 10 | - [] item 1... 11 | - [] item 2... 12 | -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/images/degree_bg_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/images/degree_bg_frame.png -------------------------------------------------------------------------------- /src/backend/joanie/tests/edx_imports/images/creative_common.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/tests/edx_imports/images/creative_common.jpeg -------------------------------------------------------------------------------- /src/frontend/admin/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from "msw/node"; 2 | import { handlers } from "./handlers"; 3 | 4 | export const server = setupServer(...handlers); 5 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/Skill.ts: -------------------------------------------------------------------------------- 1 | export type Skill = { 2 | id: string; 3 | title: string; 4 | }; 5 | 6 | export type DTOSkill = Omit; 7 | -------------------------------------------------------------------------------- /docs/assets/moodle_add_external_webservice_authorized_users_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_add_external_webservice_authorized_users_1.png -------------------------------------------------------------------------------- /docs/assets/moodle_add_external_webservice_authorized_users_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/docs/assets/moodle_add_external_webservice_authorized_users_2.png -------------------------------------------------------------------------------- /src/frontend/admin/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from "msw/browser"; 2 | import { handlers } from "./handlers"; 3 | 4 | export const worker = setupWorker(...handlers); 5 | -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/fonts/barlow/400-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/fonts/barlow/400-italic.woff2 -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/images/degree_bg_decorator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/images/degree_bg_decorator.png -------------------------------------------------------------------------------- /src/frontend/admin/src/components/presentational/breadrumbs/type.ts: -------------------------------------------------------------------------------- 1 | export interface BreadcrumbsLinkProps { 2 | name: string; 3 | href?: string; 4 | isActive?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "user_name" { 3 | type = string 4 | description = "The OpenStack name of the user Django will use to connect to the media bucket" 5 | } 6 | -------------------------------------------------------------------------------- /bin/pytest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh" 4 | 5 | _dc_run \ 6 | -e DJANGO_CONFIGURATION=Test \ 7 | app-dev \ 8 | pytest "$@" 9 | -------------------------------------------------------------------------------- /src/backend/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include src/backend/joanie *.html *.png *.gif *.css *.ico *.jpg *.jpeg *.po *.mo *.eot *.svg *.ttf *.woff *.woff2 4 | -------------------------------------------------------------------------------- /src/frontend/admin/jest.polyfills.js: -------------------------------------------------------------------------------- 1 | import { Blob, File } from "node:buffer"; 2 | Object.defineProperties(globalThis, { 3 | Blob: { value: Blob }, 4 | File: { value: File }, 5 | }); 6 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/GeneratedCertificate.ts: -------------------------------------------------------------------------------- 1 | export type GeneratedCertificate = { 2 | id: string; 3 | definition_title: string; 4 | issued_on: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/frontend/admin/src/utils/testing.ts: -------------------------------------------------------------------------------- 1 | import process from "process"; 2 | 3 | export const isTestEnv = (): boolean => { 4 | return process.env.NEXT_PUBLIC_API_SOURCE === "test"; 5 | }; 6 | -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/images/degree_fun_verified_badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/images/degree_fun_verified_badge.png -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/images/default_degree_bg_decorator_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/images/default_degree_bg_decorator_en.png -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/images/default_degree_bg_decorator_fr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/images/default_degree_bg_decorator_fr.png -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/images/unicamp_degree_bg_decorator_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/images/unicamp_degree_bg_decorator_en.png -------------------------------------------------------------------------------- /src/backend/joanie/core/static/joanie/images/unicamp_degree_bg_decorator_fr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/core/static/joanie/images/unicamp_degree_bg_decorator_fr.png -------------------------------------------------------------------------------- /src/backend/joanie/core/filters/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=wildcard-import,unused-wildcard-import 2 | """API Resource Filters of core application""" 3 | 4 | from .admin import * 5 | from .client import * 6 | -------------------------------------------------------------------------------- /src/backend/joanie/core/views/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-wildcard-import 2 | """Views of the ``core`` app of the Joanie project.""" 3 | 4 | from .certificate import * 5 | from .redirect import * 6 | -------------------------------------------------------------------------------- /src/backend/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Setup file for the joanie module. All configuration stands in the setup.cfg file.""" 3 | # coding: utf-8 4 | 5 | from setuptools import setup 6 | 7 | setup() 8 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/Teacher.ts: -------------------------------------------------------------------------------- 1 | export type Teacher = { 2 | id: string; 3 | first_name: string; 4 | last_name: string; 5 | }; 6 | 7 | export type DTOTeacher = Omit; 8 | -------------------------------------------------------------------------------- /src/frontend/admin/src/utils/createEmotionCache.ts: -------------------------------------------------------------------------------- 1 | import createCache from "@emotion/cache"; 2 | 3 | export default function createEmotionCache() { 4 | return createCache({ key: "css", prepend: true }); 5 | } 6 | -------------------------------------------------------------------------------- /src/backend/joanie/core/flows/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-import 2 | """ 3 | Viewflow Flows of Joanie "Core" application 4 | https://github.com/viewflow/viewflow 5 | """ 6 | 7 | from .order import OrderFlow 8 | -------------------------------------------------------------------------------- /src/mail/html-to-text.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "wordwrap": 600, 3 | "selectors": [ 4 | { 5 | "selector": "h1", 6 | "options": { 7 | "uppercase": false 8 | } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/utils/__diff__/microcredential_degree_default_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/tests/core/utils/__diff__/microcredential_degree_default_original.png -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/utils/__diff__/microcredential_degree_unicamp_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openfun/joanie/HEAD/src/backend/joanie/tests/core/utils/__diff__/microcredential_degree_unicamp_original.png -------------------------------------------------------------------------------- /src/frontend/admin/src/components/templates/courses/form/types.ts: -------------------------------------------------------------------------------- 1 | export interface ProductRelationModalData { 2 | name?: string | undefined; 3 | course?: any | undefined; 4 | organizations?: any[] | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /src/frontend/admin/src/layouts/dashboard/nav/item/DashboardNavItem.module.scss: -------------------------------------------------------------------------------- 1 | @use "@/styles/abstracts/" as *; 2 | 3 | .dashboardNavItem { 4 | min-width: 32px; 5 | svg { 6 | font-size: rem-calc(18px); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/backend/joanie/core/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=wildcard-import,unused-wildcard-import 2 | """Serializers for Joanie Core app.""" 3 | 4 | from .admin import * 5 | from .base import * 6 | from .client import * 7 | -------------------------------------------------------------------------------- /src/frontend/admin/src/types/api.ts: -------------------------------------------------------------------------------- 1 | import { Nullable } from "./utils"; 2 | 3 | export interface PaginatedResponse { 4 | count: number; 5 | next: Nullable; 6 | previous: Nullable; 7 | results: Array; 8 | } 9 | -------------------------------------------------------------------------------- /src/frontend/admin/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /src/test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | /test-results/ 7 | /test-results/ 8 | /playwright-report/ 9 | /blob-report/ 10 | /playwright/.cache/ 11 | -------------------------------------------------------------------------------- /src/backend/joanie/core/utils/course_run/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-import 2 | """Course run utils module""" 3 | 4 | from .aggregate_course_runs_dates import aggregate_course_runs_dates 5 | from .get_course_run_metrics import get_course_run_metrics 6 | -------------------------------------------------------------------------------- /src/frontend/admin/src/hooks/form/useFormSubmit.ts: -------------------------------------------------------------------------------- 1 | import { Maybe } from "@/types/utils"; 2 | 3 | export const useFormSubmit = (formEntity: Maybe) => { 4 | return { 5 | showSubmit: !formEntity, 6 | enableAutoSave: !!formEntity, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/frontend/admin/src/utils/string.ts: -------------------------------------------------------------------------------- 1 | import { Maybe, Nullable } from "@/types/utils"; 2 | 3 | export const removeEOL = (str: Maybe>): string => { 4 | if (!str) { 5 | return ""; 6 | } 7 | return str?.replace(/(\r)/gm, ""); 8 | }; 9 | -------------------------------------------------------------------------------- /src/frontend/admin/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/frontend/admin/src/types/routes.ts: -------------------------------------------------------------------------------- 1 | export interface BaseEntityRoutesPaths { 2 | getAll: (params?: string) => string; 3 | get: (id: string, params?: string) => string; 4 | create: string; 5 | update: (id: string) => string; 6 | delete: (id: string) => string; 7 | } 8 | -------------------------------------------------------------------------------- /src/frontend/admin/src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | @use "abstracts"; 2 | 3 | body { 4 | overflow: auto !important; 5 | margin: 0; 6 | } 7 | 8 | .flex-row { 9 | display: flex; 10 | flex-direction: row; 11 | } 12 | 13 | .align-center { 14 | align-items: center; 15 | } 16 | -------------------------------------------------------------------------------- /env.d/development/postgresql: -------------------------------------------------------------------------------- 1 | # Postgresql db container configuration 2 | POSTGRES_DB=joanie 3 | POSTGRES_USER=fun 4 | POSTGRES_PASSWORD=pass 5 | 6 | # App database configuration 7 | DB_HOST=postgresql 8 | DB_NAME=joanie 9 | DB_USER=fun 10 | DB_PASSWORD=pass 11 | DB_PORT=5432 12 | -------------------------------------------------------------------------------- /src/frontend/admin/src/layouts/dashboard/nav/item/list/DashboardNavItemsList.module.scss: -------------------------------------------------------------------------------- 1 | .navListSubHeader { 2 | background-color: transparent; 3 | padding-left: 10px; 4 | margin-bottom: 12px; 5 | } 6 | 7 | .dashboardNavListContainer { 8 | margin-bottom: 15px; 9 | } 10 | -------------------------------------------------------------------------------- /src/terraform/output.tf: -------------------------------------------------------------------------------- 1 | output "objectstorage_bucket_name" { 2 | value = openstack_objectstorage_container_v1.joanie_media_storage.name 3 | } 4 | 5 | output "contracts_objectstorage_bucket_name" { 6 | value = openstack_objectstorage_container_v1.joanie_contracts_storage.name 7 | } 8 | -------------------------------------------------------------------------------- /src/frontend/admin/jest.env.js: -------------------------------------------------------------------------------- 1 | // Override some environment variables for tests 2 | process.env.NEXT_PUBLIC_API_ENDPOINT = "http://localhost:8071/api/v1.0/admin"; 3 | process.env.NEXT_PUBLIC_DJANGO_ADMIN_BASE_URL = "http://localhost:8071/admin"; 4 | process.env.NEXT_PUBLIC_API_SOURCE = "mocked"; 5 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/Image.ts: -------------------------------------------------------------------------------- 1 | export type ImageDetailField = { 2 | filename: string; 3 | height: number; 4 | width: number; 5 | src: string; 6 | size: number; 7 | }; 8 | 9 | export type ThumbnailDetailField = ImageDetailField & { 10 | srcset?: string; 11 | }; 12 | -------------------------------------------------------------------------------- /src/mail/bin/mjml-to-html: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run mjml command to convert all mjml templates to html files 4 | DIR_MAILS="../backend/joanie/core/templates/mail/html/" 5 | 6 | if [ ! -d "${DIR_MAILS}" ]; then 7 | mkdir -p "${DIR_MAILS}"; 8 | fi 9 | mjml mjml/*.mjml -o "${DIR_MAILS}"; 10 | -------------------------------------------------------------------------------- /src/frontend/admin/src/tests/orders/OrderListItemTestScenario.ts: -------------------------------------------------------------------------------- 1 | import { OrderListItemFactory } from "@/services/factories/orders"; 2 | 3 | export const getOrderListItemsScenarioStore = (itemsNumber: number = 10) => { 4 | const list = OrderListItemFactory(itemsNumber); 5 | return { 6 | list, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/frontend/admin/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export const initMocks = async () => { 2 | if (typeof window === "undefined") { 3 | const { server } = await import("./server"); 4 | await server.listen(); 5 | } else { 6 | const { worker } = await import("./browser"); 7 | await worker.start(); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /env.d/development/localtunnel.dist: -------------------------------------------------------------------------------- 1 | ## Locatunnel configuration 2 | 3 | LOCALTUNNEL_PORT=8071 4 | LOCALTUNNEL_SUBDOMAIN=dev-${USER}-joanie 5 | DOMAIN=localtunnel.me 6 | 7 | LOCALTUNNEL_HOST=https://${DOMAIN} 8 | LOCALTUNNEL_DOMAIN=${LOCALTUNNEL_SUBDOMAIN}.${DOMAIN} 9 | LOCALTUNNEL_URL=https://${LOCALTUNNEL_DOMAIN} 10 | -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block userlinks %} 5 | {% trans "Back office" %} / 6 | {% trans "Sentry tools" %} / 7 | {{block.super}} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /src/frontend/admin/src/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | export const randomNumber = (max: number): number => { 2 | return Math.floor(Math.random() * max) + 1; 3 | }; 4 | 5 | export const toDigitString = (value: number) => { 6 | if (value >= 10) { 7 | return value.toString(); 8 | } 9 | 10 | return `0${value}`; 11 | }; 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/tray/templates/services/app/deploy_celery.yml.j2: -------------------------------------------------------------------------------- 1 | {% set service_variant = "celery" %} 2 | {% set joanie_replicas = joanie_celery_replicas %} 3 | {% set joanie_livenessprobe = joanie_celery_livenessprobe %} 4 | {% set joanie_readynessprobe = joanie_celery_readynessprobe %} 5 | 6 | {% include "./_deploy_base.yml.j2" with context %} 7 | -------------------------------------------------------------------------------- /src/backend/joanie/badges/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for the badges app.""" 2 | 3 | 4 | class AuthenticationError(Exception): 5 | """Raised when a Badge provider credentials are not valid.""" 6 | 7 | 8 | class BadgeProviderError(Exception): 9 | """Generic error raised when a Badge provider encounter an exception.""" 10 | -------------------------------------------------------------------------------- /src/backend/joanie/core/context_processors/admin.py: -------------------------------------------------------------------------------- 1 | """Admin context processors.""" 2 | 3 | from django.conf import settings 4 | 5 | 6 | def settings_processors(request): 7 | """Bind settings value to context for admin views purpose.""" 8 | return { 9 | "ADMIN_BACKOFFICE_URL": settings.JOANIE_BACKOFFICE_BASE_URL, 10 | } 11 | -------------------------------------------------------------------------------- /src/frontend/admin/src/hooks/useFromIdSearchParams.ts: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from "next/navigation"; 2 | import { Maybe } from "@/types/utils"; 3 | 4 | export const useFromIdSearchParams = (): Maybe => { 5 | const searchParams = useSearchParams(); 6 | const fromId = searchParams.get("from"); 7 | return fromId ?? undefined; 8 | }; 9 | -------------------------------------------------------------------------------- /src/frontend/admin/src/theme/types.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | declare module "@mui/material/styles" { 3 | interface Theme { 4 | navigation: { 5 | width: number; 6 | }; 7 | } 8 | // allow configuration using `createTheme` 9 | interface ThemeOptions { 10 | navigation?: { 11 | width: number; 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/tray/templates/services/app/secret.yml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | labels: 5 | app: joanie 6 | service: joanie 7 | name: "{{ joanie_secret_name }}" 8 | namespace: "{{ namespace_name }}" 9 | data: 10 | {% for k, v in JOANIE_VAULT.items() %} 11 | {{ k }}: {{ v | default('') | b64encode }} 12 | {% endfor %} 13 | -------------------------------------------------------------------------------- /src/frontend/admin/src/layouts/dashboard/header/DashboardLayoutHeader.module.scss: -------------------------------------------------------------------------------- 1 | .headerContainer { 2 | display: flex; 3 | justify-content: space-between; 4 | align-items: center; 5 | } 6 | 7 | .openNavigationButton { 8 | border-radius: 5px; 9 | transform: rotate(180deg); 10 | } 11 | 12 | .navigationIsOpen { 13 | transform: rotate(0deg); 14 | } 15 | -------------------------------------------------------------------------------- /src/frontend/admin/src/tests/batch-orders/BatchOrderListItemTestScenario.ts: -------------------------------------------------------------------------------- 1 | import { BatchOrderListItemFactory } from "@/services/factories/batch-orders"; 2 | 3 | export const getBatchOrderListItemsScenarioStore = ( 4 | itemsNumber: number = 10, 5 | ) => { 6 | const list = BatchOrderListItemFactory(itemsNumber); 7 | return { 8 | list, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/presentational/menu-popover/types.ts: -------------------------------------------------------------------------------- 1 | export type MenuPopoverArrowValue = 2 | | "top-left" 3 | | "top-center" 4 | | "top-right" 5 | | "bottom-left" 6 | | "bottom-center" 7 | | "bottom-right" 8 | | "left-top" 9 | | "left-center" 10 | | "left-bottom" 11 | | "right-top" 12 | | "right-center" 13 | | "right-bottom"; 14 | -------------------------------------------------------------------------------- /src/openApiClientJs/scripts/openapi-typescript-codegen/generate_api_client_local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # usage: yarn generate:api:client:local [--output] 4 | 5 | # OPTIONS: 6 | # --output the path folder where types will be generated 7 | 8 | openapi --input http://localhost:8071/v1.0/swagger/?format=openapi --output $1 --indent='2' --name ApiClientJoanie --useOptions 9 | -------------------------------------------------------------------------------- /src/terraform/state.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | openstack = { 4 | source = "terraform-provider-openstack/openstack" 5 | version = "1.46.0" 6 | } 7 | } 8 | 9 | backend "swift" { 10 | container = "joanie-terraform" 11 | archive_container = "joanie-terraform-archive" 12 | } 13 | 14 | required_version = ">= 1.0.0" 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/admin/playwright/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Testing Page 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function Index() { 6 | const { pathname, replace } = useRouter(); 7 | 8 | useEffect(() => { 9 | replace(PATH_ADMIN.rootAdmin); 10 | }, [pathname]); 11 | 12 | return null; 13 | } 14 | -------------------------------------------------------------------------------- /src/frontend/admin/src/utils/array.ts: -------------------------------------------------------------------------------- 1 | export const mergeArrayUnique = ( 2 | firstArray: T[], 3 | secondArray: T[], 4 | predicate = (first: T, second: T) => first === second, 5 | ) => { 6 | const result = [...firstArray]; 7 | secondArray.forEach((bItem) => 8 | result.some((cItem) => predicate(bItem, cItem)) ? null : result.push(bItem), 9 | ); 10 | return result; 11 | }; 12 | -------------------------------------------------------------------------------- /src/tray/templates/services/nginx/configs/healthcheck.conf.j2: -------------------------------------------------------------------------------- 1 | server { 2 | listen {{ joanie_nginx_healthcheck_port }}; 3 | server_name localhost; 4 | 5 | location {{ joanie_nginx_healthcheck_endpoint }} { 6 | add_header Content-Type text/plain; 7 | return 200 "OK"; 8 | } 9 | 10 | location {{ joanie_nginx_status_endpoint }} { 11 | stub_status; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/contract_definition/fragment_course_plan.html: -------------------------------------------------------------------------------- 1 |
  • 2 |

    {{ plan.name }}

    3 | {% if plan.children %} 4 |
      5 | {% for child in plan.children|dictsort:"position" %} 6 | {% include "contract_definition/fragment_course_plan.html" with plan=child %} 7 | {% endfor %} 8 |
    9 | {% endif %} 10 |
  • 11 | -------------------------------------------------------------------------------- /src/mail/mjml/partial/footer.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% blocktranslate with href=site.url name=site.name trimmed %} 5 | This mail has been sent to {{email}} by {{name}} 6 | {% endblocktranslate %} 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/frontend/admin/src/types/auth.ts: -------------------------------------------------------------------------------- 1 | export type AuthenticatedUser = { 2 | full_name: string; 3 | id: string; 4 | is_staff: boolean; 5 | is_superuser: boolean; 6 | username: string; 7 | abilities: { 8 | delete: boolean; 9 | get: boolean; 10 | has_course_access: boolean; 11 | has_organization_access: boolean; 12 | patch: boolean; 13 | put: boolean; 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/tray/templates/services/admin/configs/healthcheck.conf.j2: -------------------------------------------------------------------------------- 1 | server { 2 | listen {{ joanie_admin_nginx_healthcheck_port }}; 3 | server_name localhost; 4 | 5 | location {{ joanie_admin_nginx_healthcheck_endpoint }} { 6 | add_header Content-Type text/plain; 7 | return 200 "OK"; 8 | } 9 | 10 | location {{ joanie_admin_nginx_status_endpoint }} { 11 | stub_status; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/backend/joanie/core/pagination.py: -------------------------------------------------------------------------------- 1 | """Pagination used by django rest framework.""" 2 | 3 | from rest_framework.pagination import PageNumberPagination 4 | 5 | 6 | class Pagination(PageNumberPagination): 7 | """Pagination to display no more than 100 objects per page sorted by creation date.""" 8 | 9 | ordering = "-created_on" 10 | max_page_size = 100 11 | page_size_query_param = "page_size" 12 | -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/issuers/contract_definition_unicamp.html: -------------------------------------------------------------------------------- 1 | {% extends "issuers/contract_definition_default.html" %} 2 | 3 | {% load extra_tags %} 4 | 5 | {% block header %} 6 | {% include 'contract_definition/fragment_logo.html' with organization=organization %} 7 | 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/frontend/admin/src/utils/dates.tsx: -------------------------------------------------------------------------------- 1 | import { getDjangoLang } from "@/utils/lang"; 2 | import { isTestEnv } from "@/utils/testing"; 3 | 4 | export const formatShortDate = (isoDate: string): string => { 5 | return new Intl.DateTimeFormat(getDjangoLang(), { 6 | dateStyle: "short", 7 | timeStyle: "short", 8 | ...(isTestEnv() ? { timeZone: "UTC" } : {}), 9 | }).format(new Date(isoDate)); 10 | }; 11 | -------------------------------------------------------------------------------- /src/backend/joanie/lms_handler/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | API routes exposed by our LMS handler app. 3 | """ 4 | 5 | from django.urls import re_path 6 | 7 | from rest_framework import routers 8 | 9 | from joanie.lms_handler.api import course_runs_sync 10 | 11 | ROUTER = routers.SimpleRouter() 12 | 13 | urlpatterns = ROUTER.urls + [ 14 | re_path("course-runs-sync/?$", course_runs_sync, name="course-runs-sync"), 15 | ] 16 | -------------------------------------------------------------------------------- /src/frontend/admin/.env.example: -------------------------------------------------------------------------------- 1 | # This file is used for local development only 2 | 3 | # -- API 4 | # Set the following variable to "mocked" to use the mocked API using MSW. 5 | # Set the following variable to "backend" to use joanie API. 6 | NEXT_PUBLIC_API_SOURCE= 7 | NEXT_PUBLIC_API_ENDPOINT=http://localhost:8071/api/v1.0/admin 8 | NEXT_PUBLIC_DJANGO_ADMIN_BASE_URL=http://localhost:8071/admin 9 | NEXT_PUBLIC_LANG=en 10 | -------------------------------------------------------------------------------- /src/backend/joanie/badges/apps.py: -------------------------------------------------------------------------------- 1 | """Joanie Badges application""" 2 | 3 | from django.apps import AppConfig 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class BadgesConfig(AppConfig): 8 | """Configuration class for the joanie badges app.""" 9 | 10 | default_auto_field = "django.db.models.BigAutoField" 11 | name = "joanie.badges" 12 | verbose_name = _("Joanie's badges application") 13 | -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/issuers/professional_training_agreement_unicamp.html: -------------------------------------------------------------------------------- 1 | {% extends "issuers/professional_training_agreement_default.html" %} 2 | 3 | {% load extra_tags %} 4 | 5 | {% block header %} 6 | {% include 'contract_definition/fragment_logo.html' with organization=organization %} 7 | 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/http/HttpError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An error to raise when a request failed. 3 | * It has been designed to store response.status and response.statusText. 4 | */ 5 | export class HttpError extends Error { 6 | code: number; 7 | data: any; 8 | 9 | constructor(status: number, statusText: string, data: any) { 10 | super(statusText); 11 | this.code = status; 12 | this.data = data; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/tray/templates/services/postgresql/secret.yml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | labels: 5 | app: joanie 6 | service: postgresql 7 | name: "{{ joanie_database_secret_name }}" 8 | namespace: "{{ namespace_name }}" 9 | data: 10 | POSTGRES_USER: "{{ JOANIE_VAULT.DB_USER | default('joanie_user') | b64encode }}" 11 | POSTGRES_PASSWORD: "{{ JOANIE_VAULT.DB_PASSWORD | default('pass') | b64encode }}" 12 | -------------------------------------------------------------------------------- /src/backend/joanie/signature/backends/__init__.py: -------------------------------------------------------------------------------- 1 | """Get Signature Backend""" 2 | 3 | from django.conf import settings 4 | from django.utils.module_loading import import_string 5 | 6 | 7 | def get_signature_backend(): 8 | """Instantiate a signature backend through `JOANIE_SIGNATURE_BACKEND` settings.""" 9 | signature_backend = getattr(settings, "JOANIE_SIGNATURE_BACKEND", None) 10 | return import_string(signature_backend)() 11 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import InitColorSchemeScript from "@mui/material/InitColorSchemeScript"; 2 | import { Head, Html, Main, NextScript } from "next/document"; 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Joanie's sandbox management script. 4 | """ 5 | 6 | import os 7 | import sys 8 | 9 | if __name__ == "__main__": 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "joanie.settings") 11 | os.environ.setdefault("DJANGO_CONFIGURATION", "Development") 12 | 13 | from configurations.management import execute_from_command_line 14 | 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /src/backend/joanie/core/utils/newsletter/__init__.py: -------------------------------------------------------------------------------- 1 | """Newsletter client utils.""" 2 | 3 | from django.conf import settings 4 | from django.utils.module_loading import import_string 5 | 6 | 7 | def get_newsletter_client(): 8 | """ 9 | Get the newsletter client. 10 | """ 11 | return ( 12 | import_string(settings.JOANIE_NEWSLETTER_CLIENT) 13 | if settings.JOANIE_NEWSLETTER_CLIENT 14 | else None 15 | ) 16 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/repositories/auth/AuthRepository.ts: -------------------------------------------------------------------------------- 1 | import { checkStatus, fetchApi } from "@/services/http/HttpService"; 2 | import { AuthenticatedUser } from "@/types/auth"; 3 | 4 | export const authApiRoutes = { 5 | me: `/users/me/`, 6 | }; 7 | 8 | export class AuthRepository { 9 | static async me(): Promise { 10 | return fetchApi(authApiRoutes.me, { method: "GET" }).then(checkStatus); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/frontend/admin/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const TRANSLATE_CONTENT_LANGUAGE = "translateContentLanguage"; 2 | export const FORCE_TRANSLATE_CONTENT_LANGUAGE = "forceTranslateContentLanguage"; 3 | export const USER_LANGUAGE = "userLang"; 4 | export const DEFAULT_PAGE_SIZE = 20; 5 | export const DEFAULT_SEARCH_DEBOUNCE = 300; 6 | export const DJANGO_LANGUAGE_COOKIE_NAME = "django_language"; 7 | export const DJANGO_SAVED_LANGUAGE = "django_language_saved"; 8 | -------------------------------------------------------------------------------- /docker/files/usr/local/etc/gunicorn/joanie.py: -------------------------------------------------------------------------------- 1 | # Gunicorn-django settings 2 | bind = ["0.0.0.0:8000"] 3 | name = "joanie" 4 | python_path = "/app" 5 | 6 | # Run 7 | graceful_timeout = 90 8 | timeout = 90 9 | workers = 3 10 | 11 | # Logging 12 | # Using '-' for the access log file makes gunicorn log accesses to stdout 13 | accesslog = "-" 14 | # Using '-' for the error log file makes gunicorn log errors to stderr 15 | errorlog = "-" 16 | loglevel = "info" 17 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/presentational/card/SimpleCard.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import { SimpleCard } from "@/components/presentational/card/SimpleCard"; 3 | 4 | describe("", () => { 5 | it("renders", async () => { 6 | render( 7 | 8 |
    Hello !
    9 |
    , 10 | ); 11 | 12 | screen.getByText("Hello !"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/Accesses.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@/services/api/models/User"; 2 | 3 | export type Accesses = { 4 | id: string; 5 | role: Roles; 6 | user: User; 7 | }; 8 | 9 | export type DTOAccesses = { 10 | user_id?: string; 11 | role: Roles; 12 | }; 13 | 14 | export type AvailableAccess = { 15 | value: string; 16 | display_name: string; 17 | }; 18 | -------------------------------------------------------------------------------- /src/backend/joanie/core/models/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=wildcard-import,unused-wildcard-import 2 | """Make models.py a module containing one file per model as it was getting too long.""" 3 | 4 | from .accounts import * 5 | from .activity_logs import * 6 | from .certifications import * 7 | from .contracts import * 8 | from .course_wishes import * 9 | from .courses import * 10 | from .products import * 11 | from .quotes import * 12 | from .site import * 13 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/orders/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function Index() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.orders.root) { 10 | push(PATH_ADMIN.orders.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/joanie/edx_imports/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | API routes exposed for server to server. Requires a specific token to request. 3 | """ 4 | 5 | from django.conf import settings 6 | from django.urls import path 7 | 8 | from joanie.edx_imports.api import course_run_view 9 | 10 | urlpatterns = [ 11 | path( 12 | f"api/{settings.API_VERSION}/edx_imports/course-run/", 13 | course_run_view, 14 | name="edx_imports_course_run", 15 | ) 16 | ] 17 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/test_models_course_access.perf.yml: -------------------------------------------------------------------------------- 1 | CourseAccessModelsTestCase.test_models_course_access_get_abilities_for_manager_of_manager: 2 | - db: 'SELECT "joanie_course_access"."role" FROM "joanie_course_access" WHERE ("joanie_course_access"."course_id" = #::uuid AND "joanie_course_access"."user_id" = #::uuid) ORDER BY "joanie_course_access"."created_on" DESC LIMIT #' 3 | CourseAccessModelsTestCase.test_models_course_access_get_abilities_preset_role: [] 4 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/courses/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect } from "react"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function Index() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.courses.root) { 10 | push(PATH_ADMIN.courses.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/products/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function Index() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.products.root) { 10 | push(PATH_ADMIN.products.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/vouchers/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function Index() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.vouchers.root) { 10 | push(PATH_ADMIN.vouchers.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /docker/files/etc/nginx/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 8082; 4 | server_name localhost; 5 | charset utf-8; 6 | 7 | location /media { 8 | alias /data/media; 9 | } 10 | 11 | location / { 12 | proxy_pass http://app:8000; 13 | proxy_set_header Host $host; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/backend/joanie/demo/defaults.py: -------------------------------------------------------------------------------- 1 | """Parameters that define how the demo site will be built.""" 2 | 3 | DEFAULT_DEMO_DOMAIN = "localhost:8071" 4 | 5 | NB_OBJECTS = { 6 | "organizations": 50, 7 | "products": 50, 8 | "courses": 100, 9 | "users": 10000, 10 | "enrollments": 30000, 11 | "max_orders_per_product": 50, 12 | } 13 | 14 | NB_DEV_OBJECTS = { 15 | "product_credential": 5, 16 | "product_certificate": 5, 17 | "course": 10, 18 | } 19 | -------------------------------------------------------------------------------- /src/backend/joanie/edx_imports/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-import 2 | """Module for importing tasks.""" 3 | 4 | from .certificates import ( 5 | import_certificates_batch_task, 6 | populate_signatory_certificates_task, 7 | ) 8 | from .course_runs import import_course_runs_batch_task 9 | from .enrollments import import_enrollments_batch_task 10 | from .universities import import_universities_batch_task 11 | from .users import import_users_batch_task 12 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/courses-runs/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect } from "react"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function Index() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.courses_run.root) { 10 | push(PATH_ADMIN.courses_run.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/enrollments/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function Index() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.enrollments.root) { 10 | push(PATH_ADMIN.enrollments.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/joanie/payment/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | API routes exposed by our Payment app 3 | """ 4 | 5 | from django.urls import re_path 6 | 7 | from rest_framework.routers import DefaultRouter 8 | 9 | from joanie.payment import api 10 | 11 | router = DefaultRouter() 12 | router.register("credit-cards", api.CreditCardViewSet, basename="credit-cards") 13 | 14 | urlpatterns = router.urls + [ 15 | re_path(r"payments/notifications/?$", api.webhook, name="payment_webhook"), 16 | ] 17 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/organizations/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function Index() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.organizations.root) { 10 | push(PATH_ADMIN.organizations.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/test_models_organization.perf.yml: -------------------------------------------------------------------------------- 1 | OrganizationModelsTestCase.test_models_organization_get_abilities_member_user: 2 | - db: 'SELECT "joanie_organization_access"."role" FROM "joanie_organization_access" WHERE ("joanie_organization_access"."organization_id" = #::uuid AND "joanie_organization_access"."user_id" = #::uuid) ORDER BY "joanie_organization_access"."created_on" DESC LIMIT #' 3 | OrganizationModelsTestCase.test_models_organization_get_abilities_preset_role: [] 4 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/certificates-definitions/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect } from "react"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function Certificates() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.certificates.root) { 10 | push(PATH_ADMIN.certificates.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/joanie/signature/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | API routes exposed by our Signature app 3 | """ 4 | 5 | from django.urls import re_path 6 | 7 | from rest_framework.routers import DefaultRouter 8 | 9 | from joanie.signature import api 10 | 11 | router = DefaultRouter() 12 | 13 | urlpatterns = router.urls + [ 14 | # Incoming webhook events from the signature provider 15 | re_path( 16 | r"signature/notifications/?$", api.webhook_signature, name="webhook_signature" 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /docker/images/admin/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.18-slim as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY src/frontend/admin . 6 | ARG NEXT_PUBLIC_API_SOURCE=backend 7 | ARG NEXT_PUBLIC_API_ENDPOINT 8 | ARG NEXT_PUBLIC_DJANGO_ADMIN_BASE_URL 9 | 10 | RUN yarn install --frozen-lockfile && \ 11 | yarn build 12 | 13 | FROM nginxinc/nginx-unprivileged:1.25 as production 14 | 15 | COPY docker/files/admin/etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf 16 | COPY --from=builder /app/out /app/admin 17 | -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/issuers/microcredential_degree_unicamp.html: -------------------------------------------------------------------------------- 1 | {% extends "issuers/microcredential_degree_default.html" %} 2 | 3 | {% load extra_tags %} 4 | 5 | {% block header %} 6 |
    7 | 8 | {% if organizations.0.logo %} 9 | {% include 'certificate/fragment_logo.html' with organization=organizations.0 %} 10 | {% endif %} 11 |
    12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /src/backend/joanie/core/utils/discount.py: -------------------------------------------------------------------------------- 1 | """Utils for discount calculation.""" 2 | 3 | from stockholm import Money, Number, Rate 4 | 5 | 6 | def calculate_price(price, discount): 7 | """ 8 | Calculate the discounted price. 9 | """ 10 | price = Money(price) 11 | discount_amount = ( 12 | Money(discount.amount) 13 | if discount.amount 14 | else price * Rate(Number(discount.rate)) 15 | ) 16 | return round(Money(price - discount_amount).as_decimal(), 2) 17 | -------------------------------------------------------------------------------- /src/frontend/admin/src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | /** 4 | * Hook which stores the previous value of a component prop or state. 5 | * https://usehooks.com/usePrevious/ 6 | * 7 | * @param value 8 | */ 9 | const usePrevious = (value: T): T => { 10 | const previous = useRef(value); 11 | 12 | useEffect(() => { 13 | previous.current = value; 14 | }, [value]); 15 | 16 | return previous.current; 17 | }; 18 | 19 | export default usePrevious; 20 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/quotes-definitions/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect } from "react"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function QuoteDefinitions() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.quote_definition.root) { 10 | push(PATH_ADMIN.quote_definition.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/joanie/core/utils/billing_address.py: -------------------------------------------------------------------------------- 1 | """Utility class to create a Billing Address object for a company""" 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class CompanyBillingAddress: 8 | """Small class to create company billing address for payments""" 9 | 10 | address: str 11 | postcode: str 12 | city: str 13 | country: str 14 | language: str 15 | first_name: str # The fullname is in `first_name` field in Joanie 16 | last_name: str = "" 17 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/presentational/button/CloseNotificationButton.tsx: -------------------------------------------------------------------------------- 1 | import CloseIcon from "@mui/icons-material/Close"; 2 | import { closeSnackbar, SnackbarKey } from "notistack"; 3 | import * as React from "react"; 4 | 5 | export function CloseNotificationButton(key: SnackbarKey) { 6 | return ( 7 | closeSnackbar(key)} 12 | /> 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/frontend/admin/src/pages/admin/contracts-definitions/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect } from "react"; 3 | import { PATH_ADMIN } from "@/utils/routes/path"; 4 | 5 | export default function ContractDefinitions() { 6 | const { pathname, push } = useRouter(); 7 | 8 | useEffect(() => { 9 | if (pathname === PATH_ADMIN.contract_definition.root) { 10 | push(PATH_ADMIN.contract_definition.list); 11 | } 12 | }, [pathname]); 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/test_models_organization_access.perf.yml: -------------------------------------------------------------------------------- 1 | OrganizationAccessModelsTestCase.test_models_organization_access_get_abilities_for_member_of_member_user: 2 | - db: 'SELECT "joanie_organization_access"."role" FROM "joanie_organization_access" WHERE ("joanie_organization_access"."organization_id" = #::uuid AND "joanie_organization_access"."user_id" = #::uuid) ORDER BY "joanie_organization_access"."created_on" DESC LIMIT #' 3 | OrganizationAccessModelsTestCase.test_models_organization_access_get_abilities_preset_role: [] 4 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0074_alter_quote_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.23 on 2025-07-28 16:10 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0073_product_quote_definition'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='quote', 15 | options={'ordering': ['-created_on'], 'verbose_name': 'Quote', 'verbose_name_plural': 'Quotes'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/backend/joanie/core/storages.py: -------------------------------------------------------------------------------- 1 | """Module containing specific storages.""" 2 | 3 | from django.conf import settings 4 | 5 | from storages.backends.s3 import S3Storage 6 | 7 | 8 | # pylint: disable=abstract-method 9 | class JoanieEasyThumbnailS3Storage(S3Storage): 10 | """ 11 | Storage used by easy thumbnail and defined in the settings THUMBNAIL_DEFAULT_STORAGE. 12 | It uses the settings shared with the default storage. Only the location is redefined. 13 | """ 14 | 15 | location = settings.THUMBNAIL_STORAGE_S3_LOCATION 16 | -------------------------------------------------------------------------------- /src/backend/joanie/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for joanie project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from configurations.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "joanie.settings") 15 | os.environ.setdefault("DJANGO_CONFIGURATION", "Development") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__ 3 | *.pyc 4 | **/__pycache__ 5 | **/*.pyc 6 | venv 7 | .venv 8 | 9 | # System-specific files 10 | .DS_Store 11 | **/.DS_Store 12 | 13 | # Docker 14 | docker-compose.* 15 | env.d 16 | 17 | # next 18 | **/.next 19 | **/node_modules 20 | **/.env 21 | src/frontend/admin/out 22 | 23 | # Docs 24 | docs 25 | *.md 26 | *.log 27 | 28 | # Development/test cache & configurations 29 | data 30 | .cache 31 | .circleci 32 | .git 33 | .vscode 34 | .iml 35 | .idea 36 | db.sqlite3 37 | .mypy_cache 38 | .pylint.d 39 | .pytest_cache 40 | -------------------------------------------------------------------------------- /bin/sqlacodegen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | BASE_DIR=$(dirname "${BASH_SOURCE[0]}") 4 | source "${BASE_DIR}/_config.sh" 5 | source "${BASE_DIR}/../env.d/development/common" 6 | 7 | _dc_run \ 8 | -e DJANGO_CONFIGURATION=Test \ 9 | app-dev \ 10 | sqlacodegen "$@" --generator=declarative \ 11 | --tables user_api_userpreference,auth_userprofile \ 12 | --outfile joanie/lms_handler/edx_imports/edx_models_to_add.py \ 13 | "mysql+pymysql://$EDX_DATABASE_USER:$EDX_DATABASE_PASSWORD@$EDX_DATABASE_HOST:$EDX_DATABASE_PORT/$EDX_DATABASE_NAME" 14 | -------------------------------------------------------------------------------- /src/frontend/admin/src/hooks/useAllLanguages/useAllLanguages.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { CoursesRunsRepository } from "@/services/repositories/courses-runs/CoursesRunsRepository"; 3 | 4 | export const useAllLanguages = () => { 5 | const languages = useQuery({ 6 | queryKey: ["allLanguages"], 7 | staleTime: Infinity, 8 | gcTime: Infinity, 9 | queryFn: async () => { 10 | return CoursesRunsRepository.getAllLanguages(); 11 | }, 12 | }); 13 | 14 | return languages?.data ?? undefined; 15 | }; 16 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/factories/skill/index.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | import { Skill } from "@/services/api/models/Skill"; 3 | 4 | function build(): Skill { 5 | return { 6 | id: faker.string.uuid(), 7 | title: faker.lorem.words({ min: 1, max: 3 }), 8 | }; 9 | } 10 | 11 | export function SkillFactory(): Skill; 12 | export function SkillFactory(count: number): Skill[]; 13 | export function SkillFactory(count?: number): Skill | Skill[] { 14 | if (count) return [...Array(count)].map(build); 15 | return build(); 16 | } 17 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/payment/lyra/responses/cancel_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "webService": "Token/Cancel", 3 | "version": "V4", 4 | "applicationVersion": "6.14.3", 5 | "status": "SUCCESS", 6 | "answer": { 7 | "responseCode": 0, 8 | "_type": "V4/Common/ResponseCodeAnswer" 9 | }, 10 | "ticket": "04812a9ed3ea4d2491ad42b1ba02ffac", 11 | "serverDate": "2024-04-15T12:59:58+00:00", 12 | "applicationProvider": "LYRA", 13 | "metadata": null, 14 | "mode": "TEST", 15 | "serverUrl": "https://api.lyra.com", 16 | "_type": "V4/WebService/Response" 17 | } 18 | -------------------------------------------------------------------------------- /src/terraform/create_state_bucket/state.tf: -------------------------------------------------------------------------------- 1 | # A separate terraform project that just creates the bucket where 2 | # we will store the state. It needs to be created before the other 3 | # project because that's where the other project will store its 4 | # state. The state is encrypted using a KMS key because it will 5 | # contain sensitive information. 6 | 7 | terraform { 8 | required_providers { 9 | openstack = { 10 | source = "terraform-provider-openstack/openstack" 11 | version = "1.46.0" 12 | } 13 | } 14 | required_version = ">= 1.0.0" 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0045_contractdefinition_appendix.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-10-21 15:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0044_alter_certificatedefinition_template'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='contractdefinition', 15 | name='appendix', 16 | field=models.TextField(blank=True, verbose_name='appendix'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/frontend/admin/mocks/handlers/users/index.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { buildApiUrl } from "@/services/http/HttpService"; 3 | import { userRoutes } from "@/services/repositories/Users/UsersRepository"; 4 | import { UsersFactory } from "@/services/factories/users"; 5 | 6 | export const userHandlers = [ 7 | http.get(buildApiUrl(userRoutes.getAll()), () => { 8 | return HttpResponse.json(UsersFactory(10)); 9 | }), 10 | http.get(buildApiUrl(userRoutes.get(":id")), () => { 11 | return HttpResponse.json(UsersFactory()); 12 | }), 13 | ]; 14 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/presentational/tooltip/ConditionalTooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PropsWithChildren } from "react"; 3 | import Tooltip, { TooltipProps } from "@mui/material/Tooltip"; 4 | 5 | type Props = TooltipProps & { 6 | enableTooltip: boolean; 7 | }; 8 | export function ConditionalTooltip({ 9 | enableTooltip = true, 10 | children, 11 | ...tooltipProps 12 | }: PropsWithChildren) { 13 | if (!enableTooltip) { 14 | return children; 15 | } 16 | 17 | return {children}; 18 | } 19 | -------------------------------------------------------------------------------- /src/backend/joanie/core/context_processors/contract_definition.py: -------------------------------------------------------------------------------- 1 | """Context processors for contract definition.""" 2 | 3 | from joanie.core.utils.contract_context_processors import parse_richie_syllabus 4 | 5 | 6 | def richie_syllabus(context): 7 | """ 8 | This processor is in charge to retrieve/parse syllabus RDF attributes 9 | from a remote Richie instance. 10 | """ 11 | course_code = context["course"]["code"] 12 | contract_language = context["contract"]["language"] 13 | 14 | return {"syllabus": parse_richie_syllabus(course_code, contract_language)} 15 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/presentational/link/CustomLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PropsWithChildren } from "react"; 3 | import Link, { LinkProps } from "@mui/material/Link"; 4 | import NextLink from "next/link"; 5 | 6 | type Props = LinkProps; 7 | export function CustomLink({ children, ...props }: PropsWithChildren) { 8 | return ( 9 | 16 | {children} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/testing/utils.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | 3 | export const delay = (ms: number) => 4 | new Promise((resolve) => setTimeout(resolve, ms)); 5 | 6 | export const closeAllNotification = async (page: Page) => { 7 | const allNotifications = await page.getByTestId("close-notification").all(); 8 | await Promise.all( 9 | allNotifications.reverse().map(async () => { 10 | const re = await page.getByTestId("close-notification").all(); 11 | await re[0].click(); 12 | await delay(200); 13 | }), 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test helpers""" 2 | 3 | from datetime import datetime 4 | 5 | 6 | def format_date(value: datetime) -> str | None: 7 | """Format a datetime to be used in a json response""" 8 | try: 9 | return value.isoformat().replace("+00:00", "Z") 10 | except AttributeError: 11 | return None 12 | 13 | 14 | def format_date_export(value: datetime) -> str: 15 | """Format a datetime to be used in a csv export""" 16 | try: 17 | return value.strftime("%d/%m/%Y %H:%M:%S") 18 | except AttributeError: 19 | return "" 20 | -------------------------------------------------------------------------------- /src/tray/templates/services/nginx/secret.yml.j2: -------------------------------------------------------------------------------- 1 | {% if activate_http_basic_auth or joanie_activate_http_basic_auth %} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | labels: 6 | app: joanie 7 | service: "nginx" 8 | name: "{{ joanie_nginx_htpasswd_secret_name }}" 9 | namespace: "{{ namespace_name }}" 10 | data: 11 | # nota bene: the {{ app.name }}_htpasswd variable is set in 12 | # tasks/get_vault_for_app.yml tasks list only if the pointed file exists 13 | "{{ http_basic_auth_user_file | basename }}": "{{ lookup('file', joanie_htpasswd) | b64encode }}" 14 | {% endif %} 15 | -------------------------------------------------------------------------------- /src/frontend/admin/src/tests/vouchers/VouchersTestScenario.ts: -------------------------------------------------------------------------------- 1 | import { Voucher } from "@/services/api/models/Voucher"; 2 | import { VoucherFactory } from "@/services/factories/voucher"; 3 | import { Discount } from "@/services/api/models/Discount"; 4 | import { DiscountFactory } from "@/services/factories/discounts"; 5 | 6 | export const getVouchersScenarioStore = (itemsNumber: number = 10) => { 7 | const list: Voucher[] = VoucherFactory(itemsNumber) as Voucher[]; 8 | const discounts: Discount[] = DiscountFactory(3); 9 | 10 | return { 11 | list, 12 | discounts, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/tray/templates/services/postgresql/ep.yml.j2: -------------------------------------------------------------------------------- 1 | {% if endpoint_postgresql_ip | ipaddr != false %} 2 | apiVersion: v1 3 | kind: Endpoints 4 | metadata: 5 | labels: 6 | app: joanie 7 | endpoint: postgresql 8 | deployment_stamp: "{{ deployment_stamp }}" 9 | # name of the endpoint should be the same as the corresponding service 10 | name: "joanie-postgresql-{{ deployment_stamp }}" 11 | namespace: "{{ namespace_name }}" 12 | subsets: 13 | - addresses: 14 | - ip: "{{ endpoint_postgresql_ip }}" 15 | ports: 16 | - port: 5432 17 | name: 5432-tcp 18 | {% endif %} 19 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/QuoteDefinition.ts: -------------------------------------------------------------------------------- 1 | import { Optional } from "@/types/utils"; 2 | 3 | export type QuoteDefinition = { 4 | id: string; 5 | title: string; 6 | description: string; 7 | body: string; 8 | language: string; 9 | name: QuoteDefinitionTemplate; 10 | }; 11 | 12 | export type QuoteDefinitionFormValues = Omit & { 13 | name: QuoteDefinitionTemplate | ""; 14 | }; 15 | 16 | export type DTOQuoteDefinition = Optional; 17 | 18 | export enum QuoteDefinitionTemplate { 19 | DEFAULT = "quote_default", 20 | } 21 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0012_producttranslation_instructions.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-20 08:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0011_remove_courseproductrelation_max_validated_orders_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="producttranslation", 14 | name="instructions", 15 | field=models.TextField(blank=True, verbose_name="instructions"), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/debug/sentry_decrypt.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/index.html" %} 2 | 3 | {% block content %} 4 |
    5 |
    6 | {% csrf_token %} 7 | 8 | 9 |
    10 | 11 | {% if decrypted %} 12 |

    Decrypted

    13 |
    {{ decrypted | pprint }}
    14 | {% endif %} 15 |
    16 | {% endblock %} 17 | 18 | {% block sidebar %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/presentational/card/SimpleCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PropsWithChildren } from "react"; 3 | import Paper, { PaperProps } from "@mui/material/Paper"; 4 | 5 | export function SimpleCard({ 6 | sx, 7 | children, 8 | ...props 9 | }: PropsWithChildren) { 10 | return ( 11 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/CertificateDefinition.ts: -------------------------------------------------------------------------------- 1 | import { Optional, ToFormValues } from "@/types/utils"; 2 | 3 | export type CertificateDefinition = { 4 | id: string; 5 | name: string; 6 | title: string; 7 | description?: string; 8 | template?: string; 9 | }; 10 | 11 | export type DTOCertificateDefinition = Optional; 12 | 13 | export type CertificateDefinitionFormValues = ToFormValues< 14 | Omit 15 | >; 16 | 17 | export enum CertificationDefinitionTemplate { 18 | CERTIFICATE = "certificate", 19 | DEGREE = "degree", 20 | } 21 | -------------------------------------------------------------------------------- /src/frontend/admin/src/translations/common/languageTranslations.ts: -------------------------------------------------------------------------------- 1 | import { defineMessages } from "react-intl"; 2 | import { LocalesEnum } from "@/types/i18n/LocalesEnum"; 3 | 4 | export const languageTranslations = defineMessages({ 5 | [LocalesEnum.ENGLISH]: { 6 | id: "translations.common.languageTranslations.english", 7 | defaultMessage: "English", 8 | description: "Common english label", 9 | }, 10 | [LocalesEnum.FRENCH]: { 11 | id: "translations.common.languageTranslations.french", 12 | defaultMessage: "French", 13 | description: "Common french label", 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0029_user_has_subscribed_to_commercial_newsletter.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-04-03 08:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0028_activitylog'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='has_subscribed_to_commercial_newsletter', 16 | field=models.BooleanField(default=False, verbose_name='has subscribed to commercial newsletter'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/test_api_address.perf.yml: -------------------------------------------------------------------------------- 1 | AddressAPITestCase.test_api_address_get_addresses: 2 | - db: 'SELECT COUNT(*) AS "__count" FROM "joanie_address" INNER JOIN "joanie_user" ON ("joanie_address"."owner_id" = "joanie_user"."id") WHERE ("joanie_address"."is_reusable" AND "joanie_user"."username" = #)' 3 | - db: 'SELECT ... FROM "joanie_address" INNER JOIN "joanie_user" ON ("joanie_address"."owner_id" = "joanie_user"."id") WHERE ("joanie_address"."is_reusable" AND "joanie_user"."username" = #) ORDER BY "joanie_address"."created_on" DESC LIMIT #' 4 | AddressAPITestCase.test_api_address_get_addresses_for_new_user: [] 5 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/factories/teacher/index.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | import { Teacher } from "@/services/api/models/Teacher"; 3 | 4 | function build(): Teacher { 5 | return { 6 | id: faker.string.uuid(), 7 | first_name: faker.person.firstName(), 8 | last_name: faker.person.lastName(), 9 | }; 10 | } 11 | 12 | export function TeacherFactory(): Teacher; 13 | export function TeacherFactory(count: number): Teacher[]; 14 | export function TeacherFactory(count?: number): Teacher | Teacher[] { 15 | if (count) return [...Array(count)].map(build); 16 | return build(); 17 | } 18 | -------------------------------------------------------------------------------- /src/backend/joanie/payment/migrations/0002_alter_creditcard_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.10 on 2023-04-21 08:43 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("payment", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="creditcard", 14 | options={ 15 | "ordering": ["-created_on"], 16 | "verbose_name": "credit card", 17 | "verbose_name_plural": "credit cards", 18 | }, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/backend/joanie/payment/migrations/0011_remove_creditcard_is_main_remove_creditcard_owner.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.18 on 2025-02-18 08:55 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('payment', '0010_creditcardownership_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='creditcard', 15 | name='is_main', 16 | ), 17 | migrations.RemoveField( 18 | model_name='creditcard', 19 | name='owner', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/templates/courses/form/sections/offering/OfferingsSection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Course } from "@/services/api/models/Course"; 3 | import { OfferingList } from "@/components/templates/offerings/offering/OfferingList"; 4 | 5 | type Props = { 6 | course: Course; 7 | invalidateCourse: () => void; 8 | }; 9 | 10 | export function OfferingsSection({ course, invalidateCourse }: Props) { 11 | return ( 12 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/repositories/AbstractRepository.ts: -------------------------------------------------------------------------------- 1 | import { ResourcesQuery } from "@/hooks/useResources"; 2 | import { PaginatedResponse } from "@/types/api"; 3 | import { Maybe } from "@/types/utils"; 4 | 5 | export interface AbstractRepository< 6 | T, 7 | Filters extends ResourcesQuery, 8 | DTOData, 9 | > { 10 | getAll: (filters?: Maybe) => Promise>; 11 | get: (id: string, filters?: Maybe) => Promise; 12 | create: (payload: DTOData) => Promise; 13 | update: (id: string, payload: DTOData) => Promise; 14 | delete: (id: string) => Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/factories/users/index.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | import { User } from "@/services/api/models/User"; 3 | 4 | const build = (): User => { 5 | return { 6 | id: faker.string.uuid(), 7 | username: faker.internet.username(), 8 | full_name: faker.person.fullName(), 9 | email: faker.internet.email(), 10 | }; 11 | }; 12 | 13 | export function UsersFactory(): User; 14 | export function UsersFactory(count: number): User[]; 15 | export function UsersFactory(count?: number): User | User[] { 16 | if (count) return [...Array(count)].map(build); 17 | return build(); 18 | } 19 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/CourseRun.ts: -------------------------------------------------------------------------------- 1 | import type { Course, CourseState } from "./Course"; 2 | 3 | export type CourseRun = { 4 | id: string; 5 | title: string; 6 | course: Course; 7 | resource_link: string; 8 | start?: string | null; 9 | end?: string | null; 10 | enrollment_end?: string | null; 11 | enrollment_start?: string | null; 12 | languages: string[]; 13 | is_gradable: boolean; 14 | is_listed: boolean; 15 | state?: CourseState; 16 | uri?: string; 17 | }; 18 | 19 | export interface DTOCourseRun extends Omit { 20 | id?: string; 21 | course_id: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/frontend/admin/src/translations/enrollments/enrollment-state.ts: -------------------------------------------------------------------------------- 1 | import { defineMessages } from "react-intl"; 2 | import { EnrollmentState } from "@/services/api/models/Enrollment"; 3 | 4 | export const enrollmentStateMessages = defineMessages({ 5 | [EnrollmentState.SET]: { 6 | id: "translations.enrollment.state.set", 7 | defaultMessage: "Set", 8 | description: "Label for the SET enrollment state", 9 | }, 10 | [EnrollmentState.FAILED]: { 11 | id: "translations.enrollment.state.failed", 12 | defaultMessage: "Failed", 13 | description: "Label for the FAILED enrollment state", 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/frontend/admin/src/translations/pages/batch-orders/breadcrumbsTranslations.ts: -------------------------------------------------------------------------------- 1 | import { defineMessages } from "react-intl"; 2 | 3 | export const batchOrdersBreadcrumbsTranslation = defineMessages({ 4 | rootBreadcrumb: { 5 | id: "pages.admin.batchOrders.breadcrumbsTranslations.rootBreadcrumb", 6 | defaultMessage: "Batch orders", 7 | description: "Breadcrumb label for the batch orders root page", 8 | }, 9 | listBreadcrumb: { 10 | id: "pages.admin.batchOrders.breadcrumbsTranslations.listBreadcrumb", 11 | defaultMessage: "List", 12 | description: "Breadcrumb label for the batch orders list page", 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/presentational/loading/LoadingContent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PropsWithChildren } from "react"; 3 | import Box from "@mui/material/Box"; 4 | import CircularProgress from "@mui/material/CircularProgress"; 5 | 6 | type Props = { 7 | loading: boolean; 8 | }; 9 | export function LoadingContent({ 10 | children, 11 | loading, 12 | }: PropsWithChildren) { 13 | if (loading) { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | return
    {children}
    ; 21 | } 22 | -------------------------------------------------------------------------------- /src/frontend/admin/src/tests/mocks/certificate-definitions/certificate-definition-mocks.ts: -------------------------------------------------------------------------------- 1 | export const CERTIFICATE_DEFINITION_OPTIONS_REQUEST_RESULT = { 2 | actions: { 3 | POST: { 4 | template: { 5 | type: "choice", 6 | required: false, 7 | read_only: false, 8 | label: "Template to generate pdf", 9 | choices: [ 10 | { 11 | value: "certificate", 12 | display_name: "Certificate", 13 | }, 14 | { 15 | value: "degree", 16 | display_name: "Degree", 17 | }, 18 | ], 19 | }, 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /bin/update_openapi_schema: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh" 4 | 5 | _dc_run \ 6 | -e DJANGO_CONFIGURATION=Test \ 7 | app-dev \ 8 | python manage.py spectacular \ 9 | --api-version 'v1.0' \ 10 | --urlconf 'joanie.client_urls' \ 11 | --format openapi-json \ 12 | --file /app/joanie/tests/swagger/swagger.json 13 | 14 | _dc_run \ 15 | -e DJANGO_CONFIGURATION=Test \ 16 | app-dev \ 17 | python manage.py spectacular \ 18 | --api-version 'v1.0' \ 19 | --urlconf 'joanie.admin_urls' \ 20 | --format openapi-json \ 21 | --file /app/joanie/tests/swagger/admin-swagger.json 22 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0065_ordergroup_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.22 on 2025-06-04 15:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0064_remove_batchorder_required_organization_if_not_draft_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='ordergroup', 15 | name='description', 16 | field=models.CharField(blank=True, help_text='Description of the order group', max_length=255, null=True, verbose_name='description'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0076_quote_reference.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.23 on 2025-08-14 15:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0075_alter_certificatedefinition_template'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='quote', 15 | name='reference', 16 | field=models.CharField(blank=True, help_text='Incremental quote reference number, e.g. FUN_2025_0000001', max_length=20, null=True, unique=True, verbose_name='reference'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/mail/bin/html-to-plain-text: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | # Run html-to-text to convert all html files to text files 4 | DIR_MAILS="../backend/joanie/core/templates/mail/" 5 | 6 | if [ ! -d "${DIR_MAILS}" ]; then 7 | mkdir -p "${DIR_MAILS}"; 8 | fi 9 | 10 | if [ ! -d "${DIR_MAILS}"html/ ]; then 11 | mkdir -p "${DIR_MAILS}"html/; 12 | exit; 13 | fi 14 | 15 | for file in "${DIR_MAILS}"html/*.html; 16 | do html-to-text -j ./html-to-text.config.json < "$file" > "${file%.html}".txt; done; 17 | 18 | if [ ! -d "${DIR_MAILS}"text/ ]; then 19 | mkdir -p "${DIR_MAILS}"text/; 20 | fi 21 | 22 | mv "${DIR_MAILS}"html/*.txt "${DIR_MAILS}"text/; 23 | -------------------------------------------------------------------------------- /src/tray/templates/services/app/svc.yml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: joanie 6 | service: app 7 | version: "{{ joanie_image_tag }}" 8 | deployment_stamp: "{{ deployment_stamp }}" 9 | name: joanie-app-{{ deployment_stamp }} # name of the service should be host name in nginx 10 | namespace: "{{ namespace_name }}" 11 | spec: 12 | ports: 13 | - name: {{ joanie_django_port }}-tcp 14 | port: {{ joanie_django_port }} 15 | protocol: TCP 16 | targetPort: {{ joanie_django_port }} 17 | selector: 18 | app: joanie 19 | deployment: "joanie-app-{{ deployment_stamp }}" 20 | type: ClusterIP 21 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0005_courseproductrelation_add_max_validated_orders.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.10 on 2023-06-01 15:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0004_course_cover_alter_organization_logo"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="courseproductrelation", 14 | name="max_validated_orders", 15 | field=models.PositiveSmallIntegerField( 16 | default=0, verbose_name="max_validated_orders" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0031_alter_order_state.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-04-17 21:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0030_order_payment_schedule'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='order', 15 | name='state', 16 | field=models.CharField(choices=[('draft', 'Draft'), ('submitted', 'Submitted'), ('pending', 'Pending'), ('canceled', 'Canceled'), ('validated', 'Validated')], db_index=True, default='draft'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0077_batchorder_payment_method.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.23 on 2025-08-05 14:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0076_quote_reference'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='batchorder', 15 | name='payment_method', 16 | field=models.CharField(choices=[('purchase_order', 'Purchase order'), ('bank_transfer', 'Bank transfer'), ('card_payment', 'Card payment')], db_index=True, default='card_payment'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/backend/joanie/payment/apps.py: -------------------------------------------------------------------------------- 1 | """Joanie Payment application""" 2 | 3 | from django.apps import AppConfig 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class PaymentConfig(AppConfig): 8 | """Configuration class for the joanie payment app.""" 9 | 10 | name = "joanie.payment" 11 | verbose_name = _("Joanie payment application") 12 | 13 | # ruff : noqa : PLC0415 14 | # pylint: disable=import-outside-toplevel, unused-import 15 | def ready(self): 16 | """Import credit card post delete receiver.""" 17 | from joanie.payment.models import ( 18 | credit_card_post_delete_receiver, 19 | ) 20 | -------------------------------------------------------------------------------- /src/frontend/admin/src/hooks/useCopyToClipboard.tsx: -------------------------------------------------------------------------------- 1 | import { useIntl } from "react-intl"; 2 | import { useSnackbar } from "notistack"; 3 | import { commonTranslations } from "@/translations/common/commonTranslations"; 4 | 5 | export const useCopyToClipboard = () => { 6 | const intl = useIntl(); 7 | const snackbar = useSnackbar(); 8 | return (str: string) => { 9 | navigator.clipboard.writeText(str).then(() => { 10 | snackbar.enqueueSnackbar( 11 | intl.formatMessage(commonTranslations.successCopy), 12 | { 13 | variant: "success", 14 | preventDuplicate: true, 15 | }, 16 | ); 17 | }); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0046_order_has_waived_withdrawal_right.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-10-22 13:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0045_contractdefinition_appendix'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='order', 15 | name='has_waived_withdrawal_right', 16 | field=models.BooleanField(default=False, editable=False, help_text='User has waived their withdrawal right.', verbose_name='has waived their right of withdrawal'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/frontend/admin/src/contexts/auth/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Maybe } from "@/types/utils"; 3 | import { AuthenticatedUser } from "@/types/auth"; 4 | 5 | export interface AuthContextInterface { 6 | user: AuthenticatedUser; 7 | setUser: (user: AuthenticatedUser) => void; 8 | } 9 | 10 | export const AuthContext = 11 | React.createContext>(undefined); 12 | 13 | export const useAuthContext = () => { 14 | const authContext = React.useContext(AuthContext); 15 | 16 | if (!authContext) { 17 | throw new Error(`useAuthContext must be used within a AuthContext`); 18 | } 19 | 20 | return authContext; 21 | }; 22 | -------------------------------------------------------------------------------- /src/frontend/admin/src/tests/mocks/quote-definitions/quote-definition-mocks.ts: -------------------------------------------------------------------------------- 1 | export const QUOTE_DEFINITION_OPTIONS_REQUEST_RESULT = { 2 | actions: { 3 | POST: { 4 | language: { 5 | choices: [ 6 | { 7 | value: "en-us", 8 | display_name: "English", 9 | }, 10 | { 11 | value: "fr-fr", 12 | display_name: "French", 13 | }, 14 | ], 15 | }, 16 | name: { 17 | choices: [ 18 | { 19 | value: "quote_default", 20 | display_name: "Quote Default", 21 | }, 22 | ], 23 | }, 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/test_utils.py: -------------------------------------------------------------------------------- 1 | """Test core utils.""" 2 | 3 | from django.test import TestCase 4 | 5 | from joanie.core.utils import merge_dict 6 | 7 | 8 | class UtilsTestCase(TestCase): 9 | """Validate that utils in the core app work as expected.""" 10 | 11 | def test_utils_merge_dict(self): 12 | """Update a deep nested dictionary with another deep nested dictionary.""" 13 | dict_1 = {"k1": {"k11": {"a": 0, "b": 1}}} 14 | dict_2 = {"k1": {"k11": {"b": 10}, "k12": {"a": 3}}} 15 | self.assertEqual( 16 | merge_dict(dict_1, dict_2), 17 | {"k1": {"k11": {"a": 0, "b": 10}, "k12": {"a": 3}}}, 18 | ) 19 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0036_order_state_migration.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.13 on 2024-05-28 09:28 2 | 3 | from django.db import migrations 4 | 5 | 6 | def migrate_order_states(apps, schema_editor): 7 | Order = apps.get_model("core", "Order") 8 | Order.objects.filter(state="validated" ).update(state="completed") 9 | Order.objects.filter(state__in=["pending", "submitted"]).update(state="canceled") 10 | 11 | 12 | class Migration(migrations.Migration): 13 | dependencies = [ 14 | ("core", "0035_order_credit_card"), 15 | ] 16 | 17 | operations = [ 18 | migrations.RunPython(migrate_order_states, migrations.RunPython.noop), 19 | ] 20 | -------------------------------------------------------------------------------- /src/backend/joanie/payment/migrations/0008_creditcard_initial_issuer_transaction_identifier.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-04-24 08:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('payment', '0007_alter_invoice_localized_context'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='creditcard', 15 | name='initial_issuer_transaction_identifier', 16 | field=models.CharField(blank=True, editable=False, max_length=50, null=True, verbose_name='initial issuer transaction identifier'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /crowdin/config.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Your crowdin's credentials 3 | # 4 | api_token_env: CROWDIN_API_TOKEN 5 | project_id_env: CROWDIN_PROJECT_ID 6 | base_path_env: CROWDIN_BASE_PATH 7 | 8 | # 9 | # Choose file structure in crowdin 10 | # e.g. true or false 11 | # 12 | preserve_hierarchy: true 13 | 14 | # 15 | # Files configuration 16 | # 17 | files: [ 18 | { 19 | source : "/backend/locale/django.pot", 20 | dest: "/backend.pot", 21 | translation : "/backend/locale/%locale_with_underscore%/LC_MESSAGES/django.po" 22 | }, 23 | { 24 | source : "/frontend/admin/i18n/frontend.json", 25 | dest: "/admin/frontend.json", 26 | translation : "/frontend/admin/i18n/locales/%locale%.json" 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /src/frontend/admin/src/layouts/dashboard/header/actions/DashboardLayoutHeaderActions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { PropsWithChildren, useEffect, useRef, useState } from "react"; 3 | import { createPortal } from "react-dom"; 4 | 5 | export function DashboardLayoutHeaderActions(props: PropsWithChildren) { 6 | const ref = useRef(null); 7 | const [mounted, setMounted] = useState(false); 8 | 9 | useEffect(() => { 10 | ref.current = document.querySelector("#header-actions"); 11 | setMounted(true); 12 | }, []); 13 | 14 | return mounted && ref.current 15 | ? createPortal(
    {props.children}
    , ref.current) 16 | : null; 17 | } 18 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/Discount.ts: -------------------------------------------------------------------------------- 1 | import { ResourcesQuery } from "@/hooks/useResources"; 2 | import { Maybe } from "@/types/utils"; 3 | 4 | export type Discount = { 5 | id: string; 6 | amount: number | null; 7 | rate: number | null; 8 | }; 9 | 10 | export type DTODiscount = Omit; 11 | 12 | export type DiscountQuery = ResourcesQuery & {}; 13 | 14 | export const getDiscountLabel = (discount: Maybe) => { 15 | if (!discount) { 16 | return ""; 17 | } 18 | if (discount.rate) { 19 | return `${discount.rate * 100}%`; 20 | } 21 | if (discount.amount) { 22 | return `${discount.amount} €`; 23 | } 24 | return discount.id; 25 | }; 26 | -------------------------------------------------------------------------------- /bin/get_tunnel_url: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | BASE_DIR=$(dirname "${BASH_SOURCE[0]}") 6 | # Using the env file is not mandatory. Variables can be already set in the current environnment 7 | 8 | if [[ -f "${BASE_DIR}/../env.d/development/localtunnel" ]]; then 9 | source "${BASE_DIR}/../env.d/development/localtunnel" 10 | fi 11 | 12 | # if LOCALTUNNEL_HOST is 'https://localtunnel.me', set DOMAIN variable to 'loca.lt' 13 | # else extract domain from LOCALTUNNEL_HOST 14 | if [[ "${LOCALTUNNEL_HOST}" == "https://localtunnel.me" ]]; then 15 | DOMAIN="loca.lt" 16 | else 17 | DOMAIN=${LOCALTUNNEL_HOST//"https://"} 18 | fi 19 | 20 | echo "https://$LOCALTUNNEL_SUBDOMAIN.$DOMAIN" 21 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0055_alter_documentimage_options_alter_ordergroup_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.19 on 2025-02-12 16:21 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0054_discount_discount_discount_rate_or_amount_required_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='documentimage', 15 | options={'ordering': ['created_on']}, 16 | ), 17 | migrations.AlterModelOptions( 18 | name='ordergroup', 19 | options={'ordering': ['created_on']}, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/mail/mjml/partial/welcome.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% if fullname %} 10 |

    11 | {% blocktranslate with name=fullname%} 12 | Hello {{ name }}, 13 | {% endblocktranslate %} 14 |

    15 | {% else %} 16 | {% trans "Hello," %} 17 | {% endif %}
    18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/presentational/modal/useModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export interface ModalUtils { 4 | open: boolean; 5 | handleOpen: () => void; 6 | handleClose: () => void; 7 | toggleModal: () => void; 8 | } 9 | export const useModal = (): ModalUtils => { 10 | const [open, setOpen] = useState(false); 11 | 12 | const handleOpen = (): void => { 13 | setOpen(true); 14 | }; 15 | 16 | const handleClose = (): void => { 17 | setOpen(false); 18 | }; 19 | 20 | const toggleModal = (): void => { 21 | setOpen(!open); 22 | }; 23 | 24 | return { 25 | open, 26 | handleOpen, 27 | handleClose, 28 | toggleModal, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/frontend/admin/src/tests/useResourceHandler.spec.tsx: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker"; 2 | import { catchAllIdRegex } from "@/tests/useResourceHandler"; 3 | 4 | describe("useRessourceHandler", () => { 5 | it("catchAllIdRegex", async () => { 6 | const routeUrl = "/test/:uuid/john/doe/:uuid/"; 7 | const testUrl = `/test/${faker.string.uuid()}/john/doe/${faker.string.uuid()}/`; 8 | let regex = catchAllIdRegex(routeUrl, ":id"); 9 | expect(regex.test(testUrl)).toEqual(false); 10 | 11 | regex = catchAllIdRegex(routeUrl, ":uuid"); 12 | expect(regex.test(testUrl)).toEqual(true); 13 | expect(regex.test(`${testUrl}?email=johndoe@yopmail.com`)).toEqual(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0044_alter_certificatedefinition_template.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.16 on 2024-10-09 08:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0043_address_unique_address_per_user_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='certificatedefinition', 15 | name='template', 16 | field=models.CharField(blank=True, choices=[('certificate', 'Certificate'), ('degree', 'Degree')], db_index=True, max_length=255, null=True, verbose_name='template to generate pdf'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/frontend/admin/src/contexts/i18n/TranslationsProvider/TranslationContext.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Maybe } from "@/types/utils"; 3 | import { LocalesEnum } from "@/types/i18n/LocalesEnum"; 4 | 5 | export interface LocaleContextInterface { 6 | currentLocale: string; 7 | setCurrentLocale: (locale: LocalesEnum) => void; 8 | } 9 | 10 | export const LocaleContext = 11 | React.createContext>(undefined); 12 | 13 | export const useLocale = () => { 14 | const localContext = React.useContext(LocaleContext); 15 | 16 | if (localContext) { 17 | return localContext; 18 | } 19 | 20 | throw new Error(`useLocale must be used within a LocaleContext`); 21 | }; 22 | -------------------------------------------------------------------------------- /src/backend/joanie/core/management/commands/synchronize_offerings.py: -------------------------------------------------------------------------------- 1 | """Synchronize offerings with the external catalog.""" 2 | 3 | import logging 4 | 5 | from django.core.management import BaseCommand 6 | 7 | from joanie.core.utils.offering import synchronize_offerings 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Command(BaseCommand): 13 | """ 14 | A command to synchronize offerings with the external catalog. 15 | """ 16 | 17 | help = __doc__ 18 | 19 | def handle(self, *args, **options): 20 | """ 21 | Handle the command to synchronize offerings. 22 | """ 23 | logger.info("Synchronizing offerings") 24 | synchronize_offerings.delay() 25 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0073_product_quote_definition.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.23 on 2025-07-24 13:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('core', '0072_remove_quote_organization_must_sign_before_buyer_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='product', 16 | name='quote_definition', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.quotedefinition', verbose_name='Quote definition'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0083_remove_batchorder_voucher_remove_order_voucher_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.23 on 2025-09-26 15:18 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('core', '0082_batchorder_signatory_email_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='batchorder', 15 | name='voucher', 16 | ), 17 | migrations.RemoveField( 18 | model_name='order', 19 | name='voucher', 20 | ), 21 | migrations.DeleteModel( 22 | name='Voucher', 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /src/frontend/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "es6", "ES2021.String"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./src/*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0079_alter_batchorder_trainees.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.23 on 2025-09-12 09:43 2 | 3 | import django.core.serializers.json 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('core', '0078_batchorder_administrative_email_and_more'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='batchorder', 16 | name='trainees', 17 | field=models.JSONField(blank=True, default=list, encoder=django.core.serializers.json.DjangoJSONEncoder, help_text='trainees name list', null=True, verbose_name='trainees'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/mail/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mail_mjml", 3 | "version": "3.1.2", 4 | "description": "An util to generate html and text django's templates from mjml templates", 5 | "type": "module", 6 | "dependencies": { 7 | "@html-to/text-cli": "0.5.4", 8 | "mjml": "4.15.3" 9 | }, 10 | "private": true, 11 | "scripts": { 12 | "build-mjml-to-html": "./bin/mjml-to-html", 13 | "build-html-to-plain-text": "./bin/html-to-plain-text", 14 | "build": "yarn build-mjml-to-html; yarn build-html-to-plain-text;" 15 | }, 16 | "volta": { 17 | "node": "20.18.2" 18 | }, 19 | "repository": "https://github.com/openfun/joanie", 20 | "author": "France Université Numérique", 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /src/backend/joanie/core/views/redirect.py: -------------------------------------------------------------------------------- 1 | """Redirect views of the `core`app.""" 2 | 3 | from django.conf import settings 4 | from django.views.generic.base import RedirectView 5 | 6 | 7 | class BackOfficeRedirectView(RedirectView): 8 | """ 9 | Redirect to the next.js backoffice application 10 | with the path caught in the redirect url 11 | """ 12 | 13 | permanent = True 14 | query_string = False 15 | pattern_name = None 16 | http_method_names = ["get"] 17 | 18 | def get_redirect_url(self, *args, **kwargs): 19 | """ 20 | Redirect to the backoffice pathname caught in the url 21 | """ 22 | return f"{settings.JOANIE_BACKOFFICE_BASE_URL}/{self.kwargs['path']}" 23 | -------------------------------------------------------------------------------- /src/frontend/admin/mocks/handlers/contract-definitions/index.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { buildApiUrl } from "@/services/http/HttpService"; 3 | import { contractDefinitionRoutes } from "@/services/repositories/contract-definition/ContractDefinitionRepository"; 4 | import { ContractDefinitionFactory } from "@/services/factories/contract-definition"; 5 | 6 | export const contractDefinitionsHandlers = [ 7 | http.get(buildApiUrl(contractDefinitionRoutes.getAll()), () => { 8 | return HttpResponse.json(ContractDefinitionFactory(10)); 9 | }), 10 | http.get(buildApiUrl(contractDefinitionRoutes.get(":id")), () => { 11 | return HttpResponse.json(ContractDefinitionFactory()); 12 | }), 13 | ]; 14 | -------------------------------------------------------------------------------- /src/frontend/admin/src/utils/arrayUtils.spec.tsx: -------------------------------------------------------------------------------- 1 | import { mergeArrayUnique } from "@/utils/array"; 2 | 3 | describe("Array Utils", () => { 4 | it("merge unique", async () => { 5 | let a = [1, 2, 3]; 6 | let b = [1, 4, 5, 2]; 7 | let result = mergeArrayUnique(a, b); 8 | expect(result).toEqual([1, 2, 3, 4, 5]); 9 | 10 | a = [1, 2, 3, 4]; 11 | b = []; 12 | result = mergeArrayUnique(a, b); 13 | expect(result).toEqual([1, 2, 3, 4]); 14 | 15 | a = []; 16 | b = [99, 100]; 17 | result = mergeArrayUnique(a, b); 18 | expect(result).toEqual([99, 100]); 19 | 20 | a = []; 21 | b = []; 22 | result = mergeArrayUnique(a, b); 23 | expect(result).toEqual([]); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/core/utils/test_file_checksum.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test suite for the util file_checksum of the joanie core app 3 | """ 4 | 5 | from django.core.files.base import ContentFile 6 | from django.test import TestCase 7 | 8 | from joanie.core.utils import file_checksum 9 | 10 | 11 | class UtilsTestCase(TestCase): 12 | """Test suite for utils.""" 13 | 14 | def test_utils_file_checksum(self): 15 | """Checksum from a file.""" 16 | file = ContentFile("") 17 | checksum = file_checksum(file) 18 | 19 | self.assertEqual(len(checksum), 64) 20 | self.assertEqual( 21 | checksum, 22 | "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 23 | ) 24 | -------------------------------------------------------------------------------- /src/frontend/admin/src/services/api/models/Voucher.ts: -------------------------------------------------------------------------------- 1 | import { Discount } from "@/services/api/models/Discount"; 2 | import { ResourcesQuery } from "@/hooks/useResources"; 3 | 4 | export type Voucher = { 5 | id: string; 6 | code: string; 7 | discount: Discount | null; 8 | multiple_use: boolean; 9 | multiple_users: boolean; 10 | orders_count: number; 11 | is_active: boolean; 12 | }; 13 | 14 | export type DTOVoucher = { 15 | id?: Voucher["id"]; 16 | code?: Voucher["code"] | null; 17 | discount_id?: Discount["id"] | null; 18 | multiple_use: Voucher["multiple_use"]; 19 | multiple_users: Voucher["multiple_users"]; 20 | is_active: Voucher["is_active"]; 21 | }; 22 | 23 | export type VoucherQuery = ResourcesQuery & {}; 24 | -------------------------------------------------------------------------------- /src/frontend/admin/src/components/templates/products/form/sections/offerings/ProductFormOfferings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Product } from "@/services/api/models/Product"; 3 | import { OfferingList } from "@/components/templates/offerings/offering/OfferingList"; 4 | import { useProducts } from "@/hooks/useProducts/useProducts"; 5 | 6 | type Props = { 7 | product: Product; 8 | }; 9 | export function ProductFormOfferings({ product }: Props) { 10 | const productRepository = useProducts({}, { enabled: false }); 11 | 12 | return ( 13 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/terraform/create_state_bucket/swift.tf: -------------------------------------------------------------------------------- 1 | resource "openstack_objectstorage_container_v1" "backend" { 2 | name = "joanie-terraform" 3 | provider = openstack.ovh 4 | 5 | # all objects should be deleted from the container so that the container 6 | # can be destroyed without error. 7 | force_destroy = true 8 | 9 | versioning { 10 | location = "joanie-terraform-archive" 11 | type = "versions" 12 | } 13 | } 14 | 15 | resource "openstack_objectstorage_container_v1" "backend_archive" { 16 | name = "joanie-terraform-archive" 17 | provider = openstack.ovh 18 | 19 | # all objects should be deleted from the container so that the container 20 | # can be destroyed without error. 21 | force_destroy = true 22 | } 23 | -------------------------------------------------------------------------------- /src/backend/joanie/core/management/commands/synchronize_brevo_subscriptions.py: -------------------------------------------------------------------------------- 1 | """Management command to synchronize brevo subscriptions.""" 2 | 3 | import logging 4 | 5 | from django.core.management import BaseCommand 6 | 7 | from joanie.core.utils.newsletter.brevo.tasks import synchronize_brevo_subscriptions 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Command(BaseCommand): 13 | """ 14 | A command to synchronize brevo subscriptions. 15 | """ 16 | 17 | help = __doc__ 18 | 19 | def handle(self, *args, **options): 20 | """ 21 | Synchronize brevo subscriptions. 22 | """ 23 | logger.info("Synchronizing brevo subscriptions") 24 | synchronize_brevo_subscriptions.delay() 25 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/payment/lyra/responses/cancel_and_refund_failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "webService": "Transaction/CancelOrRefund", 3 | "version": "V4", 4 | "applicationVersion": "6.22.1", 5 | "status": "ERROR", 6 | "answer": { 7 | "errorCode": "PSP_010", 8 | "errorMessage": "transaction not found", 9 | "detailedErrorCode": null, 10 | "detailedErrorMessage": null, 11 | "ticket": "null", 12 | "shopId": "69876357", 13 | "_type": "V4/WebService/WebServiceError" 14 | }, 15 | "ticket": "eb1720062ea64f9583b42206df0cded3", 16 | "serverDate": "2024-10-10T13:40:03+00:00", 17 | "applicationProvider": "LYRA", 18 | "metadata": null, 19 | "mode": "TEST", 20 | "serverUrl": "https://api.lyra.com", 21 | "_type": "V4/WebService/Response" 22 | } 23 | -------------------------------------------------------------------------------- /src/backend/joanie/tests/payment/lyra/responses/is_already_paid_failed.json: -------------------------------------------------------------------------------- 1 | { 2 | "webService": "Order/Get", 3 | "version": "V4", 4 | "applicationVersion": "6.25.0", 5 | "status": "ERROR", 6 | "answer": { 7 | "errorCode": "PSP_010", 8 | "errorMessage": "transaction not found", 9 | "detailedErrorCode": null, 10 | "detailedErrorMessage": null, 11 | "ticket": "null", 12 | "shopId": "69876357", 13 | "_type": "V4/WebService/WebServiceError" 14 | }, 15 | "ticket": "c90bdfb76f444a4bafa3082e289cdded", 16 | "serverDate": "2025-01-28T11:06:52+00:00", 17 | "applicationProvider": "LYRA", 18 | "metadata": null, 19 | "mode": "TEST", 20 | "serverUrl": "https://api.lyra.com", 21 | "_type": "V4/WebService/Response" 22 | } -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0030_order_payment_schedule.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-04-05 09:05 2 | 3 | from django.db import migrations, models 4 | import joanie.core.fields.schedule 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('core', '0029_user_has_subscribed_to_commercial_newsletter'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='order', 16 | name='payment_schedule', 17 | field=models.JSONField(blank=True, editable=False, encoder=joanie.core.fields.schedule.OrderPaymentScheduleEncoder, help_text='Payment schedule for the order.', null=True, verbose_name='payment schedule'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/backend/joanie/core/migrations/0035_order_credit_card.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.13 on 2024-05-23 10:52 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('payment', '0008_creditcard_initial_issuer_transaction_identifier'), 11 | ('core', '0034_alter_order_state'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='order', 17 | name='credit_card', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='payment.creditcard', verbose_name='credit card'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/backend/joanie/core/templates/debug/pdf_viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 🧑‍💻 Joanie Dev Tool - Document PDF Viewer 6 | 23 | 24 | 25 |