├── .credo.exs ├── .dockerignore ├── .env.sample ├── .formatter.exs ├── .github ├── FUNDING.yml ├── stale.yml └── workflows │ ├── docker-image.yml │ └── elixir.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── CNAME ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.txt ├── README.md ├── assets ├── build.js ├── css │ ├── app.css │ └── custom.scss ├── images │ ├── client-login.jpg │ ├── icons │ │ ├── arrow-white.svg │ │ ├── arrow.svg │ │ ├── chatbubble-ellipses-outline.svg │ │ ├── create-outline.svg │ │ ├── ellipsis-horizontal-white.svg │ │ ├── ellipsis-horizontal.svg │ │ ├── email.png │ │ ├── exit-outline.svg │ │ ├── hashtag.svg │ │ ├── menu-outline.svg │ │ ├── nft.svg │ │ ├── online-users.svg │ │ ├── reload-outline.svg │ │ ├── send.svg │ │ ├── star.svg │ │ ├── time.svg │ │ └── user.svg │ ├── logo-large-black.svg │ ├── logo-large.svg │ ├── logo-white.svg │ ├── logo.svg │ ├── mobile.png │ └── new-event-bg.png ├── js │ ├── app.js │ ├── manager.js │ └── presenter.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── tailwind.config.js └── vendor │ └── topbar.js ├── build.sh ├── charts └── claper │ ├── Chart.yaml │ └── templates │ ├── _env.yaml │ ├── autoscaling.yaml │ ├── deployment.yaml │ ├── headless.yaml │ ├── ingress.yaml │ ├── rbac.yaml │ └── service.yaml ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── dev.sh ├── docker-compose.yml ├── elixir_buildpack.config ├── lib ├── claper.ex ├── claper │ ├── accounts.ex │ ├── accounts │ │ ├── leader_notifier.ex │ │ ├── oidc │ │ │ └── user.ex │ │ ├── user.ex │ │ ├── user_notifier.ex │ │ └── user_token.ex │ ├── application.ex │ ├── embeds.ex │ ├── embeds │ │ └── embed.ex │ ├── events.ex │ ├── events │ │ ├── activity_leader.ex │ │ └── event.ex │ ├── forms.ex │ ├── forms │ │ ├── field.ex │ │ ├── form.ex │ │ └── form_submit.ex │ ├── helpers │ │ └── config.ex │ ├── interactions.ex │ ├── mailer.ex │ ├── polls.ex │ ├── polls │ │ ├── poll.ex │ │ ├── poll_opt.ex │ │ └── poll_vote.ex │ ├── posts.ex │ ├── posts │ │ ├── post.ex │ │ └── reaction.ex │ ├── presentations.ex │ ├── presentations │ │ ├── presentation_file.ex │ │ └── presentation_state.ex │ ├── quizzes.ex │ ├── quizzes │ │ ├── quiz.ex │ │ ├── quiz_question.ex │ │ ├── quiz_question_opt.ex │ │ └── quiz_response.ex │ ├── release.ex │ ├── repo.ex │ ├── schema.ex │ ├── stats.ex │ ├── stats │ │ └── stat.ex │ ├── tasks │ │ └── converter.ex │ └── workers │ │ ├── mailers.ex │ │ └── quiz_lti.ex ├── claper_web.ex ├── claper_web │ ├── channels │ │ └── presence.ex │ ├── controllers │ │ ├── event_controller.ex │ │ ├── lti │ │ │ ├── grade_controller.ex │ │ │ ├── launch_controller.ex │ │ │ └── registration_controller.ex │ │ ├── mailbox_guard.ex │ │ ├── page_controller.ex │ │ ├── post_controller.ex │ │ ├── stat_controller.ex │ │ ├── user_auth.ex │ │ ├── user_confirmation_controller.ex │ │ ├── user_oidc_auth.ex │ │ ├── user_registration_controller.ex │ │ ├── user_reset_password_controller.ex │ │ ├── user_session_controller.ex │ │ └── user_settings_controller.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── helpers.ex │ ├── live │ │ ├── attendee_live_auth.ex │ │ ├── embed_live │ │ │ ├── form_component.ex │ │ │ └── form_component.html.heex │ │ ├── event_live │ │ │ ├── embed_component.ex │ │ │ ├── embed_iframe_component.ex │ │ │ ├── event_card_component.ex │ │ │ ├── event_form_component.ex │ │ │ ├── event_form_component.html.heex │ │ │ ├── form_component.ex │ │ │ ├── index.ex │ │ │ ├── index.html.heex │ │ │ ├── join.ex │ │ │ ├── join.html.heex │ │ │ ├── manage.ex │ │ │ ├── manage.html.heex │ │ │ ├── manageable_post_component.ex │ │ │ ├── manageable_quiz_component.ex │ │ │ ├── manager_settings_component.ex │ │ │ ├── poll_component.ex │ │ │ ├── post_component.ex │ │ │ ├── presenter.ex │ │ │ ├── presenter.html.heex │ │ │ ├── quiz_component.ex │ │ │ ├── show.ex │ │ │ └── show.html.heex │ │ ├── form_live │ │ │ ├── form_component.ex │ │ │ └── form_component.html.heex │ │ ├── live_helpers.ex │ │ ├── modal_component.ex │ │ ├── poll_live │ │ │ ├── form_component.ex │ │ │ └── form_component.html.heex │ │ ├── quiz_live │ │ │ ├── quiz_component.ex │ │ │ └── quiz_component.html.heex │ │ ├── stat_live │ │ │ ├── index.ex │ │ │ └── index.html.heex │ │ ├── user_live_auth.ex │ │ └── user_settings_live │ │ │ ├── show.ex │ │ │ └── show.html.heex │ ├── notifiers │ │ ├── leader_notifier.ex │ │ └── user_notifier.ex │ ├── plugs │ │ ├── iframe.ex │ │ └── locale.ex │ ├── router.ex │ ├── telemetry.ex │ ├── templates │ │ ├── error │ │ │ ├── 404.html.heex │ │ │ ├── 500.html.heex │ │ │ └── csrf_error.html.heex │ │ ├── layout │ │ │ ├── _avatar.html.heex │ │ │ ├── _profile_dropdown.html.heex │ │ │ ├── _user_menu.html.heex │ │ │ ├── app.html.heex │ │ │ ├── email.html.heex │ │ │ ├── live.html.heex │ │ │ ├── root.html.heex │ │ │ └── user.html.heex │ │ ├── leader_notifier │ │ │ └── invitation.html.heex │ │ ├── lti │ │ │ ├── launch │ │ │ │ └── error.html.heex │ │ │ └── registration │ │ │ │ ├── new.html.heex │ │ │ │ └── success.html.heex │ │ ├── page │ │ │ ├── privacy.html.heex │ │ │ ├── tos.html.heex │ │ │ └── user_confirmation │ │ │ │ ├── edit.html.heex │ │ │ │ └── new.html.heex │ │ ├── user_confirmation │ │ │ └── new.html.heex │ │ ├── user_notifier │ │ │ ├── change.html.heex │ │ │ ├── confirm.html.heex │ │ │ ├── magic.html.heex │ │ │ ├── reset.html.heex │ │ │ └── welcome.html.heex │ │ ├── user_registration │ │ │ ├── confirm.html.heex │ │ │ └── new.html.heex │ │ ├── user_reset_password │ │ │ ├── edit.html.heex │ │ │ └── new.html.heex │ │ └── user_session │ │ │ └── new.html.heex │ └── views │ │ ├── attendee_registration_view.ex │ │ ├── component_view.ex │ │ ├── components │ │ ├── alert_component.ex │ │ └── input_component.ex │ │ ├── error_helpers.ex │ │ ├── error_view.ex │ │ ├── event_view.ex │ │ ├── layout_view.ex │ │ ├── leader_notifier_view.ex │ │ ├── lti │ │ ├── grade_view.ex │ │ ├── launch_view.ex │ │ └── registration_view.ex │ │ ├── page_view.ex │ │ ├── post_view.ex │ │ ├── user_confirmation_view.ex │ │ ├── user_notifier_view.ex │ │ ├── user_registration_view.ex │ │ ├── user_reset_password_view.ex │ │ ├── user_session_view.ex │ │ ├── user_settings_view.ex │ │ └── user_view.ex ├── lti_13.ex ├── lti_13 │ ├── deployments.ex │ ├── deployments │ │ └── deployment.ex │ ├── jwks.ex │ ├── jwks │ │ ├── jwk.ex │ │ └── utils │ │ │ ├── key_generator.ex │ │ │ └── validator.ex │ ├── nonces.ex │ ├── nonces │ │ └── nonce.ex │ ├── quiz_score_reporter.ex │ ├── registrations.ex │ ├── registrations │ │ └── registration.ex │ ├── resources.ex │ ├── resources │ │ └── resource.ex │ ├── tool │ │ ├── launch_validation.ex │ │ ├── message_validators │ │ │ ├── message_validator.ex │ │ │ └── resource_message_validator.ex │ │ ├── oidc_login.ex │ │ └── services │ │ │ ├── access_token.ex │ │ │ ├── ags.ex │ │ │ ├── ags │ │ │ ├── line_item.ex │ │ │ └── score.ex │ │ │ ├── nrps.ex │ │ │ └── nrps │ │ │ └── membership.ex │ ├── users.ex │ └── users │ │ └── user.ex └── utils │ ├── file_upload.ex │ └── simple_s3_upload.ex ├── mix.exs ├── mix.lock ├── phoenix_static_buildpack.config ├── priv ├── gettext │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── default.po │ │ │ └── errors.po │ ├── default.pot │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── default.po │ │ │ └── errors.po │ ├── errors.pot │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── default.po │ │ │ └── errors.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── default.po │ │ │ └── errors.po │ ├── it │ │ └── LC_MESSAGES │ │ │ ├── default.po │ │ │ └── errors.po │ └── nl │ │ └── LC_MESSAGES │ │ ├── default.po │ │ └── errors.po ├── repo │ ├── migrations │ │ ├── .formatter.exs │ │ ├── 20211007130631_create_users_auth_tables.exs │ │ ├── 20211007145520_create_events.exs │ │ ├── 20211007152922_create_posts.exs │ │ ├── 20220111171051_create_reactions.exs │ │ ├── 20220226210445_create_presentation_files.exs │ │ ├── 20220305222231_create_polls.exs │ │ ├── 20220305223506_create_poll_opts.exs │ │ ├── 20220314171347_create_activity_leaders.exs │ │ ├── 20220409094249_create_presentation_states.exs │ │ ├── 20220418194055_create_poll_votes.exs │ │ ├── 20220419141142_create_stats.exs │ │ ├── 20220420124141_events_add_audience_peak_column.exs │ │ ├── 20220822205711_add_hashed_password_to_users.exs │ │ ├── 20230218180723_create_forms.exs │ │ ├── 20230218181013_create_form_submits.exs │ │ ├── 20230419205637_add_multiple_from_polls.exs │ │ ├── 20230421093834_add_chat_enabled_to_presentation_states.exs │ │ ├── 20230809172244_add_anonymous_chat_enabled_to_presentation_states.exs │ │ ├── 20231020175202_add_pinned_to_posts_and_presentation_states.exs │ │ ├── 20231028144823_create_embeds.exs │ │ ├── 20240323140827_add_message_reaction_enabled_to_presentation_states.exs │ │ ├── 20240405111550_add_show_poll_results_enabled_to_presentation_states.exs │ │ ├── 20240407090614_add_locale_to_users.exs │ │ ├── 20240609150059_add_lti_tables.exs │ │ ├── 20240729152555_create_oidc_users.exs │ │ ├── 20240730123205_remove_is_admin_from_users.exs │ │ ├── 20240730123331_add_is_randomized_password_to_users.exs │ │ ├── 20240731132357_add_provider_to_embeds.exs │ │ ├── 20240801084601_add_show_results_to_polls.exs │ │ ├── 20240801084731_remove_show_poll_results_enabled_from_presentation_states.exs │ │ ├── 20240801130739_remove_uniqueness_of_hash_from_presentation_files.exs │ │ ├── 20240928085505_create_quizzes.exs │ │ ├── 20241128102850_add_attendees_columns_to_stats.exs │ │ ├── 20241207115352_add_lti_line_item_columns_to_quizzes_and_lti_resources.exs │ │ ├── 20241207162344_add_user_id_to_lti_registrations.exs │ │ ├── 20241207195849_add_oban_jobs_table.exs │ │ ├── 20241223150438_add_lti_resource_id_to_quizzes.exs │ │ ├── 20241228162732_add_deleted_at_to_users.exs │ │ └── 20250102174720_add_allow_anonymous_to_quizzes.exs │ └── seeds.exs └── static │ ├── favicon.ico │ ├── fonts │ └── Roboto │ │ ├── roboto-v29-latin-100.eot │ │ ├── roboto-v29-latin-100.svg │ │ ├── roboto-v29-latin-100.ttf │ │ ├── roboto-v29-latin-100.woff │ │ ├── roboto-v29-latin-100.woff2 │ │ ├── roboto-v29-latin-100italic.eot │ │ ├── roboto-v29-latin-100italic.svg │ │ ├── roboto-v29-latin-100italic.ttf │ │ ├── roboto-v29-latin-100italic.woff │ │ ├── roboto-v29-latin-100italic.woff2 │ │ ├── roboto-v29-latin-300.eot │ │ ├── roboto-v29-latin-300.svg │ │ ├── roboto-v29-latin-300.ttf │ │ ├── roboto-v29-latin-300.woff │ │ ├── roboto-v29-latin-300.woff2 │ │ ├── roboto-v29-latin-300italic.eot │ │ ├── roboto-v29-latin-300italic.svg │ │ ├── roboto-v29-latin-300italic.ttf │ │ ├── roboto-v29-latin-300italic.woff │ │ ├── roboto-v29-latin-300italic.woff2 │ │ ├── roboto-v29-latin-500.eot │ │ ├── roboto-v29-latin-500.svg │ │ ├── roboto-v29-latin-500.ttf │ │ ├── roboto-v29-latin-500.woff │ │ ├── roboto-v29-latin-500.woff2 │ │ ├── roboto-v29-latin-500italic.eot │ │ ├── roboto-v29-latin-500italic.svg │ │ ├── roboto-v29-latin-500italic.ttf │ │ ├── roboto-v29-latin-500italic.woff │ │ ├── roboto-v29-latin-500italic.woff2 │ │ ├── roboto-v29-latin-700.eot │ │ ├── roboto-v29-latin-700.svg │ │ ├── roboto-v29-latin-700.ttf │ │ ├── roboto-v29-latin-700.woff │ │ ├── roboto-v29-latin-700.woff2 │ │ ├── roboto-v29-latin-700italic.eot │ │ ├── roboto-v29-latin-700italic.svg │ │ ├── roboto-v29-latin-700italic.ttf │ │ ├── roboto-v29-latin-700italic.woff │ │ ├── roboto-v29-latin-700italic.woff2 │ │ ├── roboto-v29-latin-900.eot │ │ ├── roboto-v29-latin-900.svg │ │ ├── roboto-v29-latin-900.ttf │ │ ├── roboto-v29-latin-900.woff │ │ ├── roboto-v29-latin-900.woff2 │ │ ├── roboto-v29-latin-900italic.eot │ │ ├── roboto-v29-latin-900italic.svg │ │ ├── roboto-v29-latin-900italic.ttf │ │ ├── roboto-v29-latin-900italic.woff │ │ ├── roboto-v29-latin-900italic.woff2 │ │ ├── roboto-v29-latin-italic.eot │ │ ├── roboto-v29-latin-italic.svg │ │ ├── roboto-v29-latin-italic.ttf │ │ ├── roboto-v29-latin-italic.woff │ │ ├── roboto-v29-latin-italic.woff2 │ │ ├── roboto-v29-latin-regular.eot │ │ ├── roboto-v29-latin-regular.svg │ │ ├── roboto-v29-latin-regular.ttf │ │ ├── roboto-v29-latin-regular.woff │ │ ├── roboto-v29-latin-regular.woff2 │ │ └── type.xml │ ├── images │ ├── base-slide.jpg │ ├── client-login.jpg │ ├── education.jpg │ ├── emails │ │ ├── bg-white-rombo.png │ │ ├── change.png │ │ └── lock4.png │ ├── favicon.png │ ├── icons │ │ ├── arrow-white.svg │ │ ├── arrow.svg │ │ ├── calendar-clear-outline.svg │ │ ├── chatbubble-ellipses-outline.svg │ │ ├── clap.svg │ │ ├── create-outline.svg │ │ ├── danger.png │ │ ├── easel-outline.svg │ │ ├── easel.svg │ │ ├── ellipsis-horizontal-white.svg │ │ ├── ellipsis-horizontal.svg │ │ ├── email.png │ │ ├── exit-outline.svg │ │ ├── eye-outline.svg │ │ ├── eye.svg │ │ ├── hashtag-white.svg │ │ ├── hashtag.svg │ │ ├── heart.svg │ │ ├── hundred.svg │ │ ├── laugh.svg │ │ ├── lms.png │ │ ├── menu-outline.svg │ │ ├── online-users.svg │ │ ├── openid.png │ │ ├── raisehand.svg │ │ ├── reader-outline.svg │ │ ├── reload-outline.svg │ │ ├── send.svg │ │ ├── star.svg │ │ ├── thumb.svg │ │ ├── time-green.svg │ │ ├── time.svg │ │ └── user.svg │ ├── interaction-icons.png │ ├── lms-platforms.png │ ├── loading.gif │ ├── logo-large-black.svg │ ├── logo-large.png │ ├── logo-large.svg │ ├── logo-white.svg │ ├── logo.png │ ├── logo.svg │ ├── mobile.png │ ├── new-event-bg.png │ ├── partners │ │ ├── lmddc.png │ │ ├── pixilearn.png │ │ └── uccs.png │ ├── plans │ │ ├── free-plan.png │ │ ├── gold-plan.png │ │ ├── platinum-plan.png │ │ └── silver-plan.png │ └── preview.png │ └── robots.txt ├── rel ├── env.bat.eex ├── env.sh.eex ├── remote.vm.args.eex └── vm.args.eex ├── reset-db.sh └── test ├── claper ├── accounts_test.exs ├── embeds_test.exs ├── events_test.exs ├── forms_test.exs ├── polls_test.exs ├── posts_test.exs ├── presentations_test.exs └── quizzes_test.exs ├── claper_web ├── controllers │ ├── lti_controller.exs │ ├── page_controller_test.exs │ ├── user_auth_test.exs │ ├── user_confirmation_controller_test.exs │ └── user_session_controller_test.exs └── live │ ├── components │ └── event_card_component_test.exs │ ├── event_live_test.exs │ └── post_live_test.exs ├── lti_13 ├── deployments_test.exs ├── jwks │ └── utils │ │ └── key_generator_test.exs ├── jwks_test.exs ├── nonces_test.exs ├── registrations_test.exs ├── resources_test.exs ├── tool │ └── oidc_login_test.exs └── users_test.exs ├── support ├── channel_case.ex ├── conn_case.ex ├── data_case.ex ├── fixtures │ ├── accounts_fixtures.ex │ ├── embeds__fixtures.ex │ ├── events_fixtures.ex │ ├── forms_fixtures.ex │ ├── lti_13 │ │ ├── deployments_fixtures.ex │ │ ├── jwks_fixtures.ex │ │ ├── registrations_fixtures.ex │ │ └── users_fixtures.ex │ ├── polls_fixtures.ex │ ├── posts_fixtures.ex │ ├── presentations_fixtures.ex │ └── quizzes_fixtures.ex └── util_fixture.ex └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | strict: false, 6 | parse_timeout: 5000, 7 | color: true, 8 | checks: %{ 9 | disabled: [ 10 | {Credo.Check.Readability.ModuleDoc, []} 11 | ] 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | # there are valid reasons to keep the .git, namely so that you can get the 3 | # current commit hash 4 | #.git 5 | .log 6 | tmp 7 | 8 | # Mix artifacts 9 | _build 10 | deps 11 | *.ez 12 | releases 13 | 14 | # Generate on crash by the VM 15 | erl_crash.dump 16 | 17 | # Static artifacts 18 | node_modules 19 | 20 | assets/node_modules/ 21 | deps/ 22 | assets/yarn.lock -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | BASE_URL=http://localhost:4000 2 | # SAME_SITE_COOKIE=Lax 3 | # SECURE_COOKIE=false 4 | 5 | DATABASE_URL=postgres://claper:claper@db:5432/claper 6 | SECRET_KEY_BASE=0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG # Generate with `mix phx.gen.secret` 7 | # ⚠️ Don't use this exact value for SECRET_KEY_BASE or someone would be able to sign a cookie with user_id=1 and log in as the admin! 8 | 9 | # Storage configuration 10 | 11 | PRESENTATION_STORAGE=local 12 | PRESENTATION_STORAGE_DIR=/app/uploads 13 | #MAX_FILE_SIZE_MB=15 14 | 15 | #AWS_ACCESS_KEY_ID=xxx 16 | #AWS_SECRET_ACCESS_KEY=xxx 17 | #AWS_REGION=eu-west-3 18 | #AWS_PRES_BUCKET=xxx 19 | 20 | # Mail configuration 21 | 22 | MAIL_TRANSPORT=local 23 | MAIL_FROM=noreply@claper.co 24 | MAIL_FROM_NAME=Claper 25 | 26 | #SMTP_RELAY=xx.example.com 27 | #SMTP_USERNAME=johndoe@example.com 28 | #SMTP_PASSWORD=xxx 29 | #SMTP_PORT=465 30 | 31 | #ENABLE_MAILBOX_ROUTE=false 32 | #MAILBOX_USER=admin 33 | #MAILBOX_PASSWORD=admin 34 | 35 | # Claper configuration 36 | 37 | #ENABLE_ACCOUNT_CREATION=true 38 | #EMAIL_CONFIRMATION=true 39 | #ALLOW_UNLINK_EXTERNAL_PROVIDER=false 40 | #LOGOUT_REDIRECT_URL=https://google.com 41 | #GS_JPG_RESOLUTION=300x300 42 | 43 | # OIDC configuration 44 | 45 | # OIDC_PROVIDER_NAME="OpenID" 46 | # OIDC_ISSUER=https://my-idp.example/application/o/claper/ 47 | # OIDC_CLIENT_ID=XXX 48 | # OIDC_CLIENT_SECRET=XXX 49 | # OIDC_SCOPES="openid email profile" 50 | # OIDC_LOGO_URL="" 51 | # OIDC_PROPERTY_MAPPINGS="roles:custom_attributes.roles,organization:custom_attributes.organization" 52 | # OIDC_AUTO_REDIRECT_LOGIN=true -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :ecto_sql, :phoenix], 3 | subdirectories: ["priv/*/migrations"], 4 | plugins: [Phoenix.LiveView.HTMLFormatter], 5 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] 6 | ] 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ClaperCo] 2 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - roadmap 8 | # Label to use when marking an issue as stale 9 | staleLabel: wontfix 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale because it has not had 13 | recent activity. It will be closed if no further activity occurs. Thank you 14 | for your contributions. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false 17 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main", "dev" ] 6 | tags: ['v*'] 7 | 8 | jobs: 9 | 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | ImageOS: ubuntu20 16 | 17 | steps: 18 | - name: Docker meta 19 | id: meta 20 | uses: docker/metadata-action@v4 21 | with: 22 | images: ghcr.io/claperco/claper 23 | tags: | 24 | type=ref,event=branch 25 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} 26 | type=semver,pattern={{version}} 27 | type=semver,pattern={{major}}.{{minor}} 28 | type=semver,pattern={{major}} 29 | 30 | - uses: actions/checkout@v3 31 | - name: Set up QEMU 32 | id: qemu 33 | uses: docker/setup-qemu-action@v1 34 | with: 35 | image: tonistiigi/binfmt:latest 36 | platforms: all 37 | - name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v1 39 | with: 40 | buildkitd-flags: --debug 41 | driver-opts: image=moby/buildkit:v0.9.1 42 | - name: Log in to registry 43 | # This is where you will update the PAT to GITHUB_TOKEN 44 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin 45 | - name: Build and push Docker image 46 | # You may pin to the exact commit or the version. 47 | # uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a 48 | uses: docker/build-push-action@v2.10.0 49 | with: 50 | context: . 51 | platforms: linux/amd64,linux/arm64 52 | push: true 53 | tags: ${{ steps.meta.outputs.tags }} 54 | labels: ${{ steps.meta.outputs.labels }} 55 | cache-from: type=gha 56 | cache-to: type=gha,mode=max 57 | build-args: | 58 | BUILD_METADATA=${{ steps.meta.outputs.json }} 59 | ERL_FLAGS=+JPperf true -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ "main", "dev" ] 6 | tags: ['v*'] 7 | pull_request: 8 | branches: [ "main", "dev" ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | 16 | name: Build and test 17 | runs-on: ubuntu-latest 18 | 19 | services: 20 | db: 21 | image: postgres:15 22 | ports: ['5432:5432'] 23 | env: 24 | POSTGRES_PASSWORD: claper 25 | POSTGRES_USER: claper 26 | POSTGRES_DB: claper 27 | options: >- 28 | --health-cmd pg_isready 29 | --health-interval 10s 30 | --health-timeout 5s 31 | --health-retries 5 32 | env: 33 | MIX_ENV: test 34 | DATABASE_URL: postgresql://claper:claper@localhost:5432/claper 35 | SECRET_KEY_BASE: QMQE4ypfy0IC1LZI/fygZNvXHPjLslnr49EE7ftcL1wgAC0MwMLdKCVJyrvXPu8z 36 | BASE_URL: http://localhost:4000 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 0 42 | - name: Set up Elixir 43 | uses: erlef/setup-beam@v1 44 | with: 45 | elixir-version: '1.16.2' 46 | otp-version: '26' 47 | - name: Restore dependencies cache 48 | uses: actions/cache@v3 49 | with: 50 | path: deps 51 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 52 | restore-keys: ${{ runner.os }}-mix- 53 | - name: Install dependencies 54 | run: mix deps.get 55 | - name: Check Formatting 56 | run: mix format --check-formatted 57 | - name: Check Credo Warnings 58 | run: mix credo diff --from-git-merge-base origin/main 59 | - name: Run tests 60 | run: mix test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | claper-*.tar 24 | 25 | # Ignore assets that are produced by build tools. 26 | /priv/static/assets/ 27 | 28 | /priv/static/uploads 29 | 30 | # Ignore digested assets cache. 31 | /priv/static/cache_manifest.json 32 | 33 | # In case you use Node.js/npm, you want to ignore these. 34 | npm-debug.log 35 | /assets/node_modules/ 36 | 37 | priv/static/images/.DS_Store 38 | priv/static/.DS_Store 39 | .env 40 | priv/static/fonts/.DS_Store 41 | test/e2e/node_modules 42 | .DS_Store 43 | priv/static/.well-known/apple-developer-merchantid-domain-association 44 | priv/static/loaderio-eb3b956a176cdd4f54eb8570ce8bbb06.txt 45 | .elixir_ls 46 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 26.2.5.6 2 | elixir 1.18.0-otp-26 3 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | docs.claper.co -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 2 | 3 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 4 | Don't forget to give the project a star! Thanks again! 5 | 6 | 1. Fork the Project 7 | 2. Create your Feature Branch (`git checkout -b feature/amazing_feature`) 8 | 3. Commit your Changes (`git commit -m 'Add some amazing feature'`) 9 | 4. Push to the Branch (`git push origin feature/amazing_feature`) 10 | 5. Open a Pull Request on `dev` branch 11 | 12 | ## Translations 13 | 14 | You can contribute to the translations by editing the files in `/priv/gettext/` 15 | Each language has its own directory with the `.po` files. The country code is used as the directory name and following the [ISO 639-1](https://en.wikipedia.org/wiki/ISO_639-1) nomenclature, for example, `en` for English, `fr` for French, `de` for German. You can find the list of country codes [here](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes). 16 | 17 | ### Add new language 18 | 19 | To add a new language, you can copy the `en` directory and rename it with the country code of the new language. Then you can edit the `.po` files with the translations. 20 | -------------------------------------------------------------------------------- /assets/build.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild') 2 | 3 | const args = process.argv.slice(2) 4 | const watch = args.includes('--watch') 5 | const deploy = args.includes('--deploy') 6 | 7 | const loader = { 8 | // Add loaders for images/fonts/etc, e.g. { '.svg': 'file' } 9 | } 10 | 11 | const plugins = [ 12 | // Add and configure plugins here 13 | ] 14 | 15 | let opts = { 16 | entryPoints: ['js/app.js'], 17 | bundle: true, 18 | target: 'es2016', 19 | outdir: '../priv/static/assets', 20 | logLevel: 'info', 21 | loader, 22 | plugins 23 | } 24 | 25 | if (watch) { 26 | opts = { 27 | ...opts, 28 | watch, 29 | sourcemap: 'inline' 30 | } 31 | } 32 | 33 | if (deploy) { 34 | opts = { 35 | ...opts, 36 | minify: true 37 | } 38 | } 39 | 40 | const promise = esbuild.build(opts) 41 | 42 | if (watch) { 43 | promise.then(_result => { 44 | process.stdin.on('close', () => { 45 | process.exit(0) 46 | }) 47 | 48 | process.stdin.resume() 49 | }) 50 | } -------------------------------------------------------------------------------- /assets/css/custom.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | @import "../node_modules/tiny-slider/src/tiny-slider.scss"; 4 | 5 | @import "../node_modules/@sjmc11/tourguidejs/src/scss/tour.scss"; 6 | 7 | $particleSize: 20vmin; 8 | $animationDuration: 6s; 9 | $amount: 20; 10 | .background span { 11 | width: $particleSize; 12 | height: $particleSize; 13 | border-radius: $particleSize; 14 | backface-visibility: hidden; 15 | opacity: 0.5; 16 | position: absolute; 17 | animation-name: move; 18 | animation-duration: $animationDuration; 19 | animation-timing-function: linear; 20 | animation-iteration-count: infinite; 21 | $colors: ( 22 | #14bfdb, 23 | #8611ed, 24 | #b80fef 25 | ); 26 | @for $i from 1 through $amount { 27 | &:nth-child(#{$i}) { 28 | color: nth($colors, random(length($colors))); 29 | top: random(100) * 1%; 30 | left: random(100) * 1%; 31 | animation-duration: math.div(random($animationDuration * 10), 10) * 1s + 10s; 32 | animation-delay: math.div(random($animationDuration + 10s) * 10, 10) * -1s; 33 | transform-origin: (random(50) ) * 1vw (random(50)) * 1vh; 34 | $blurRadius: (random() + 0.9) * $particleSize * 0.9; 35 | $x: if(random() > 0.5, -1, 1); 36 | box-shadow: ($particleSize * 2 * $x) 0 $blurRadius currentColor; 37 | } 38 | } 39 | } 40 | 41 | @keyframes move { 42 | 100% { 43 | transform: translate3d(0, 0, 1px) rotate(360deg); 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /assets/images/client-login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/assets/images/client-login.jpg -------------------------------------------------------------------------------- /assets/images/icons/chatbubble-ellipses-outline.svg: -------------------------------------------------------------------------------- 1 | Chatbubble Ellipses -------------------------------------------------------------------------------- /assets/images/icons/create-outline.svg: -------------------------------------------------------------------------------- 1 | Create -------------------------------------------------------------------------------- /assets/images/icons/ellipsis-horizontal-white.svg: -------------------------------------------------------------------------------- 1 | Ellipsis Horizontal -------------------------------------------------------------------------------- /assets/images/icons/ellipsis-horizontal.svg: -------------------------------------------------------------------------------- 1 | Ellipsis Horizontal -------------------------------------------------------------------------------- /assets/images/icons/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/assets/images/icons/email.png -------------------------------------------------------------------------------- /assets/images/icons/exit-outline.svg: -------------------------------------------------------------------------------- 1 | Exit -------------------------------------------------------------------------------- /assets/images/icons/menu-outline.svg: -------------------------------------------------------------------------------- 1 | Menu -------------------------------------------------------------------------------- /assets/images/icons/nft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/icons/online-users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /assets/images/icons/reload-outline.svg: -------------------------------------------------------------------------------- 1 | Reload -------------------------------------------------------------------------------- /assets/images/icons/send.svg: -------------------------------------------------------------------------------- 1 | Send -------------------------------------------------------------------------------- /assets/images/icons/star.svg: -------------------------------------------------------------------------------- 1 | Star -------------------------------------------------------------------------------- /assets/images/icons/time.svg: -------------------------------------------------------------------------------- 1 | Time -------------------------------------------------------------------------------- /assets/images/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/images/logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/assets/images/mobile.png -------------------------------------------------------------------------------- /assets/images/new-event-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/assets/images/new-event-bg.png -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "deploy": "NODE_ENV=production tailwindcss --postcss --minify --input=css/app.css --output=../priv/static/assets/app.css" 4 | }, 5 | "devDependencies": { 6 | "alpinejs": "^3.13.8", 7 | "autoprefixer": "^10.4.19", 8 | "esbuild": "^0.20.2", 9 | "postcss": "^8.4.38", 10 | "postcss-import": "^15.1.0", 11 | "tailwindcss": "^3.3.3" 12 | }, 13 | "dependencies": { 14 | "@sjmc11/tourguidejs": "^0.0.16", 15 | "@tailwindcss/container-queries": "^0.1.1", 16 | "air-datepicker": "^3.5.0", 17 | "animate.css": "^4.1.1", 18 | "moment": "^2.29.4", 19 | "moment-timezone": "^0.5.43", 20 | "phoenix": "file:../deps/phoenix", 21 | "phoenix_html": "file:../deps/phoenix_html", 22 | "phoenix_live_view": "file:../deps/phoenix_live_view", 23 | "qr-code-styling": "^1.6.0-rc.1", 24 | "split-grid": "^1.0.11", 25 | "split.js": "^1.6.5", 26 | "tiny-slider": "^2.9.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | } 7 | } -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # exit on error 3 | set -o errexit 4 | 5 | # Initial setup 6 | mix deps.get --only prod 7 | MIX_ENV=prod mix compile 8 | 9 | # Compile assets 10 | mix assets.deploy 11 | 12 | # Build the release and overwrite the existing release directory 13 | MIX_ENV=prod mix release --overwrite 14 | 15 | # for auto DB migration upon deploy 16 | MIX_ENV=prod mix ecto.migrate -------------------------------------------------------------------------------- /charts/claper/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: 1.0.0 3 | description: Claper app 4 | name: claper 5 | version: 1.0.0 6 | -------------------------------------------------------------------------------- /charts/claper/templates/_env.yaml: -------------------------------------------------------------------------------- 1 | {{- define "env" -}} 2 | - name: SECRET_KEY_BASE 3 | valueFrom: 4 | secretKeyRef: 5 | name: claper-secret 6 | key: secret-key-base 7 | - name: POD_IP 8 | valueFrom: 9 | fieldRef: 10 | fieldPath: status.podIP 11 | - name: NAMESPACE 12 | valueFrom: 13 | fieldRef: 14 | fieldPath: metadata.namespace 15 | - name: ENDPOINT_HOST 16 | value: claper.co 17 | - name: DATABASE_URL 18 | value: postgresql://claper:claper@10.0.0.6:6432/claper 19 | - name: AWS_ACCESS_KEY_ID 20 | value: XXX 21 | - name: AWS_PRES_BUCKET 22 | value: XXX 23 | - name: POOL_SIZE 24 | value: "20" 25 | - name: AWS_REGION 26 | value: eu-west-3 27 | - name: AWS_SECRET_ACCESS_KEY 28 | value: XXX 29 | - name: PRESENTATION_STORAGE 30 | value: s3 31 | {{- end -}} 32 | -------------------------------------------------------------------------------- /charts/claper/templates/autoscaling.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2beta2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: claper-hpa 5 | spec: 6 | maxReplicas: 5 7 | minReplicas: 1 8 | scaleTargetRef: 9 | apiVersion: apps/v1 10 | kind: Deployment 11 | name: claper-app 12 | metrics: 13 | - type: Resource 14 | resource: 15 | name: cpu 16 | target: 17 | type: Utilization 18 | averageUtilization: 50 -------------------------------------------------------------------------------- /charts/claper/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: claper-app 5 | labels: 6 | app: claper 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: claper 12 | template: 13 | metadata: 14 | labels: 15 | app: claper 16 | spec: 17 | serviceAccountName: sa-claper 18 | containers: 19 | - name: claper 20 | image: ghcr.io/claperco/claper:latest 21 | imagePullPolicy: Always 22 | resources: 23 | requests: 24 | cpu: "300m" 25 | livenessProbe: 26 | httpGet: 27 | path: / 28 | port: 4000 29 | initialDelaySeconds: 60 30 | periodSeconds: 20 31 | ports: 32 | - containerPort: 4000 33 | - name: epmd 34 | containerPort: 4369 35 | protocol: TCP 36 | env: 37 | {{ include "env" . | indent 10 }} 38 | -------------------------------------------------------------------------------- /charts/claper/templates/headless.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: claper-svc-headless 5 | spec: 6 | ports: 7 | - port: 4369 8 | targetPort: epmd 9 | protocol: TCP 10 | name: epmd 11 | selector: 12 | app: claper 13 | clusterIP: None -------------------------------------------------------------------------------- /charts/claper/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: claper-ingress 5 | annotations: 6 | kubernetes.io/ingress.class: nginx 7 | cert-manager.io/cluster-issuer: letsencrypt-prod 8 | spec: 9 | tls: 10 | - hosts: 11 | - claper.co 12 | secretName: claper-tls 13 | rules: 14 | - host: claper.co 15 | http: 16 | paths: 17 | - pathType: Prefix 18 | path: "/" 19 | backend: 20 | service: 21 | name: claper-svc 22 | port: 23 | number: 80 24 | -------------------------------------------------------------------------------- /charts/claper/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: sa-claper 5 | --- 6 | apiVersion: rbac.authorization.k8s.io/v1 7 | kind: Role 8 | metadata: 9 | name: role-claper 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - endpoints 15 | verbs: 16 | - list 17 | - get 18 | --- 19 | apiVersion: rbac.authorization.k8s.io/v1 20 | kind: RoleBinding 21 | metadata: 22 | name: rb-claper 23 | roleRef: 24 | apiGroup: rbac.authorization.k8s.io 25 | kind: Role 26 | name: role-claper 27 | subjects: 28 | - kind: ServiceAccount 29 | name: sa-claper -------------------------------------------------------------------------------- /charts/claper/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: claper-svc 5 | labels: 6 | app: claper 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - name: cowboy 11 | port: 80 12 | targetPort: 4000 13 | - port: 4369 14 | name: epmd 15 | targetPort: 4369 16 | selector: 17 | app: claper -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with esbuild to bundle .js and .css sources. 9 | config :claper, ClaperWeb.Endpoint, 10 | check_origin: false, 11 | code_reloader: true, 12 | debug_errors: true, 13 | watchers: [ 14 | # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args) 15 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}, 16 | sass: { 17 | DartSass, 18 | :install_and_run, 19 | [:default, ~w(--embed-source-map --source-map-urls=absolute --watch)] 20 | }, 21 | npx: [ 22 | "tailwindcss", 23 | "--input=css/app.css", 24 | "--output=../priv/static/assets/app.css", 25 | "--postcss", 26 | "--watch", 27 | cd: Path.expand("../assets", __DIR__) 28 | ] 29 | ] 30 | 31 | # Watch static and templates for browser reloading. 32 | config :claper, ClaperWeb.Endpoint, 33 | live_reload: [ 34 | patterns: [ 35 | ~r"priv/static/[^uploads].*(js|css|png|jpeg|jpg|gif|svg)$", 36 | ~r"priv/gettext/.*(po)$", 37 | ~r"lib/claper_web/(live|views)/.*(ex)$", 38 | ~r"lib/claper_web/templates/.*(eex)$", 39 | ~r"assets/.*\.(js|css)$" 40 | ] 41 | ] 42 | 43 | # Do not include metadata nor timestamps in development logs 44 | config :logger, :console, format: "[$level] $message\n" 45 | 46 | # Set a higher stacktrace during development. Avoid configuring such 47 | # in production as building large stacktraces may be expensive. 48 | config :phoenix, :stacktrace_depth, 20 49 | 50 | # Initialize plugs at runtime for faster development compilation 51 | config :phoenix, :plug_init_mode, :runtime 52 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :claper, ClaperWeb.Endpoint, 4 | cache_static_manifest: "priv/static/cache_manifest.json", 5 | server: true 6 | 7 | # Do not print debug messages in production 8 | config :logger, level: :info 9 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Configure your database 4 | # 5 | # The MIX_TEST_PARTITION environment variable can be used 6 | # to provide built-in test partitioning in CI environment. 7 | # Run `mix help test` for more information. 8 | config :claper, Claper.Repo, 9 | username: "claper", 10 | password: "claper", 11 | database: "claper_test#{System.get_env("MIX_TEST_PARTITION")}", 12 | hostname: "localhost", 13 | pool: Ecto.Adapters.SQL.Sandbox, 14 | pool_size: 1 15 | 16 | # We don't run a server during test. If one is required, 17 | # you can enable the server option below. 18 | config :claper, ClaperWeb.Endpoint, 19 | http: [ip: {127, 0, 0, 1}, port: 4002], 20 | secret_key_base: "YnJKcv692Yso3lHGqaJ6kJxKBDh0BUL+mJhguLm5rzoJ+xCEuN7MdrguMSnHKoz4", 21 | server: false 22 | 23 | # In test we don't send emails. 24 | config :claper, Claper.Mailer, adapter: Swoosh.Adapters.Test 25 | 26 | config :claper, Oban, testing: :inline 27 | 28 | # Print only warnings and errors during test 29 | config :logger, level: :warning 30 | 31 | # Initialize plugs at runtime for faster test compilation 32 | config :phoenix, :plug_init_mode, :runtime 33 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | set -a 2 | source .env 3 | set +a 4 | 5 | args=("$@") 6 | 7 | if [ "${args[0]}" == "start" ]; then 8 | mix phx.server 9 | elif [ "${args[0]}" == "iex" ]; then 10 | iex -S mix 11 | else 12 | mix "$@" 13 | fi 14 | 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: postgres:15 4 | ports: 5 | - 5432:5432 6 | volumes: 7 | - "claper-db:/var/lib/postgresql/data" 8 | healthcheck: 9 | test: 10 | - CMD 11 | - pg_isready 12 | - "-q" 13 | - "-d" 14 | - "claper" 15 | - "-U" 16 | - "claper" 17 | retries: 3 18 | timeout: 5s 19 | environment: 20 | POSTGRES_PASSWORD: claper 21 | POSTGRES_USER: claper 22 | POSTGRES_DB: claper 23 | networks: 24 | - claper-net 25 | app: 26 | image: ghcr.io/claperco/claper:latest # or build: . 27 | ports: 28 | - 4000:4000 29 | volumes: 30 | - "claper-uploads:/app/uploads" 31 | healthcheck: 32 | test: curl --fail http://localhost:4000 || exit 1 33 | retries: 3 34 | start_period: 20s 35 | timeout: 5s 36 | env_file: .env 37 | depends_on: 38 | db: 39 | condition: service_healthy 40 | networks: 41 | - claper-net 42 | 43 | volumes: 44 | claper-db: 45 | driver: local 46 | claper-uploads: 47 | driver: local 48 | 49 | networks: 50 | claper-net: 51 | driver: bridge 52 | -------------------------------------------------------------------------------- /elixir_buildpack.config: -------------------------------------------------------------------------------- 1 | elixir_version=1.13.2 2 | erlang_version=24.0 3 | -------------------------------------------------------------------------------- /lib/claper.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper do 2 | @moduledoc """ 3 | Claper keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/claper/accounts/leader_notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Accounts.LeaderNotifier do 2 | def deliver_event_invitation(event_name, email, url) do 3 | Claper.Workers.Mailers.event_invitation(event_name, email, url) |> Oban.insert() 4 | 5 | {:ok, :enqueued} 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/claper/accounts/oidc/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Accounts.Oidc.User do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | sub: String.t(), 8 | name: String.t() | nil, 9 | email: String.t(), 10 | issuer: String.t(), 11 | provider: String.t(), 12 | refresh_token: String.t(), 13 | access_token: String.t(), 14 | expires_at: NaiveDateTime.t(), 15 | photo_url: String.t(), 16 | groups: {:array, :string}, 17 | roles: :string, 18 | organization: String.t(), 19 | user_id: integer(), 20 | inserted_at: NaiveDateTime.t(), 21 | updated_at: NaiveDateTime.t() 22 | } 23 | 24 | schema "oidc_users" do 25 | field :sub, :string 26 | field :name, :string 27 | field :email, :string 28 | field :issuer, :string 29 | field :provider, :string 30 | field :id_token, :string 31 | field :refresh_token, :string, redact: true 32 | field :access_token, :string, redact: true 33 | field :expires_at, :naive_datetime 34 | field :photo_url, :string 35 | field :groups, {:array, :string} 36 | field :roles, :string 37 | field :organization, :string 38 | 39 | belongs_to :user, Claper.Accounts.User 40 | 41 | timestamps() 42 | end 43 | 44 | @doc false 45 | def changeset(user, attrs) do 46 | user 47 | |> cast(attrs, [ 48 | :sub, 49 | :name, 50 | :email, 51 | :issuer, 52 | :provider, 53 | :id_token, 54 | :photo_url, 55 | :access_token, 56 | :expires_at, 57 | :groups, 58 | :organization, 59 | :user_id, 60 | :roles, 61 | :refresh_token 62 | ]) 63 | |> validate_required([:sub, :email, :issuer, :provider, :id_token, :user_id]) 64 | |> unique_constraint(:sub) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/claper/accounts/user_notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Accounts.UserNotifier do 2 | # import Swoosh.Email 3 | 4 | # Delivers the email using the application mailer. 5 | # defp deliver(recipient, subject, body) do 6 | # from_name = Application.get_env(:claper, :mail)[:from_name] 7 | # from_email = Application.get_env(:claper, :mail)[:from] 8 | 9 | # email = 10 | # new() 11 | # |> to(recipient) 12 | # |> from({from_name, from_email}) 13 | # |> subject(subject) 14 | # |> text_body(body) 15 | 16 | # with {:ok, _metadata} <- Mailer.deliver(email) do 17 | # {:ok, email} 18 | # end 19 | # end 20 | 21 | def deliver_magic_link(email, url) do 22 | Claper.Workers.Mailers.new_magic_link(email, url) |> Oban.insert() 23 | 24 | {:ok, :enqueued} 25 | end 26 | 27 | def deliver_welcome(email) do 28 | Claper.Workers.Mailers.new_welcome(email) |> Oban.insert() 29 | 30 | {:ok, :enqueued} 31 | end 32 | 33 | @doc """ 34 | Deliver instructions to confirm account. 35 | """ 36 | def deliver_confirmation_instructions(user, url) do 37 | Claper.Workers.Mailers.new_confirmation(user.id, url) |> Oban.insert() 38 | 39 | {:ok, :enqueued} 40 | end 41 | 42 | @doc """ 43 | Deliver instructions to reset a user password. 44 | """ 45 | def deliver_reset_password_instructions(user, url) do 46 | Claper.Workers.Mailers.new_reset_password(user.id, url) |> Oban.insert() 47 | 48 | {:ok, :enqueued} 49 | end 50 | 51 | @doc """ 52 | Deliver instructions to update a user email. 53 | """ 54 | def deliver_update_email_instructions(user, url) do 55 | Claper.Workers.Mailers.new_update_email(user.id, url) |> Oban.insert() 56 | 57 | {:ok, :enqueued} 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/claper/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | topologies = Application.get_env(:libcluster, :topologies) || [] 11 | oidc_config = Application.get_env(:claper, :oidc) || [] 12 | Oban.Telemetry.attach_default_logger() 13 | 14 | children = [ 15 | {Cluster.Supervisor, [topologies, [name: Claper.ClusterSupervisor]]}, 16 | # Start the Ecto repository 17 | Claper.Repo, 18 | # Start the Telemetry supervisor 19 | ClaperWeb.Telemetry, 20 | # Start the PubSub system 21 | {Phoenix.PubSub, name: Claper.PubSub}, 22 | # Start the Endpoint (http/https) 23 | ClaperWeb.Presence, 24 | ClaperWeb.Endpoint, 25 | # Start a worker by calling: Claper.Worker.start_link(arg) 26 | # {Claper.Worker, arg} 27 | {Finch, name: Swoosh.Finch}, 28 | {Task.Supervisor, name: Claper.TaskSupervisor}, 29 | {Oidcc.ProviderConfiguration.Worker, 30 | %{issuer: oidc_config[:issuer], name: Claper.OidcProviderConfig}}, 31 | {Oban, Application.fetch_env!(:claper, Oban)} 32 | ] 33 | 34 | # See https://hexdocs.pm/elixir/Supervisor.html 35 | # for other strategies and supported options 36 | opts = [strategy: :one_for_one, name: Claper.Supervisor] 37 | 38 | Supervisor.start_link(children, opts) 39 | end 40 | 41 | # Tell Phoenix to update the endpoint configuration 42 | # whenever the application is updated. 43 | @impl true 44 | def config_change(changed, _new, removed) do 45 | ClaperWeb.Endpoint.config_change(changed, removed) 46 | :ok 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/claper/forms/field.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Forms.Field do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | name: String.t(), 7 | type: String.t() 8 | } 9 | 10 | @primary_key false 11 | embedded_schema do 12 | field :name, :string 13 | field :type, :string 14 | end 15 | 16 | @doc false 17 | def changeset(form, attrs \\ %{}) do 18 | form 19 | |> cast(attrs, [:name, :type]) 20 | |> validate_required([:name, :type]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/claper/forms/form.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Forms.Form do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | enabled: boolean() | nil, 8 | position: integer() | nil, 9 | title: String.t(), 10 | fields: [Claper.Forms.Field.t()] | nil, 11 | presentation_file_id: integer() | nil, 12 | form_submits: [Claper.Forms.FormSubmit.t()] | nil, 13 | inserted_at: NaiveDateTime.t(), 14 | updated_at: NaiveDateTime.t() 15 | } 16 | 17 | @derive {Jason.Encoder, only: [:title, :position]} 18 | schema "forms" do 19 | field :enabled, :boolean, default: true 20 | field :position, :integer, default: 0 21 | field :title, :string 22 | embeds_many :fields, Claper.Forms.Field, on_replace: :delete 23 | 24 | belongs_to :presentation_file, Claper.Presentations.PresentationFile 25 | has_many :form_submits, Claper.Forms.FormSubmit, on_replace: :delete 26 | 27 | timestamps() 28 | end 29 | 30 | @doc false 31 | def changeset(form, attrs \\ %{}) do 32 | form 33 | |> cast(attrs, [:enabled, :title, :presentation_file_id, :position]) 34 | |> cast_embed(:fields) 35 | |> validate_required([:title, :presentation_file_id, :position]) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/claper/forms/form_submit.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Forms.FormSubmit do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | attendee_identifier: String.t() | nil, 8 | response: map(), 9 | form_id: integer() | nil, 10 | user_id: integer() | nil, 11 | inserted_at: NaiveDateTime.t(), 12 | updated_at: NaiveDateTime.t() 13 | } 14 | 15 | schema "form_submits" do 16 | field :attendee_identifier, :string 17 | field :response, :map, on_replace: :delete 18 | belongs_to :form, Claper.Forms.Form 19 | belongs_to :user, Claper.Accounts.User 20 | 21 | timestamps() 22 | end 23 | 24 | @doc false 25 | def changeset(form_submit, attrs) do 26 | form_submit 27 | |> cast(attrs, [:attendee_identifier, :user_id, :form_id, :response]) 28 | |> validate_required([:form_id]) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/claper/helpers/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.ConfigHelpers do 2 | def get_var_from_path_or_env(config_dir, var_name, default \\ nil) do 3 | var_path = Path.join(config_dir, var_name) 4 | 5 | if File.exists?(var_path) do 6 | File.read!(var_path) |> String.trim() 7 | else 8 | System.get_env(var_name, default) 9 | end 10 | end 11 | 12 | def get_int_from_path_or_env(config_dir, var_name, default \\ nil) do 13 | var = get_var_from_path_or_env(config_dir, var_name) 14 | 15 | case var do 16 | nil -> 17 | default 18 | 19 | var -> 20 | case Integer.parse(var) do 21 | {int, ""} -> int 22 | _ -> raise "Config variable #{var_name} must be an integer. Got #{var}" 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/claper/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Mailer do 2 | use Swoosh.Mailer, otp_app: :claper 3 | end 4 | -------------------------------------------------------------------------------- /lib/claper/polls/poll.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Polls.Poll do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | title: String.t(), 8 | position: integer() | nil, 9 | total: integer() | nil, 10 | enabled: boolean() | nil, 11 | multiple: boolean() | nil, 12 | presentation_file_id: integer() | nil, 13 | poll_opts: [Claper.Polls.PollOpt.t()], 14 | poll_votes: [Claper.Polls.PollVote.t()] | nil, 15 | show_results: boolean() | nil, 16 | inserted_at: NaiveDateTime.t(), 17 | updated_at: NaiveDateTime.t() 18 | } 19 | 20 | @derive {Jason.Encoder, only: [:title, :position]} 21 | schema "polls" do 22 | field :title, :string 23 | field :position, :integer 24 | field :total, :integer, virtual: true 25 | field :enabled, :boolean 26 | field :multiple, :boolean 27 | field :show_results, :boolean 28 | 29 | belongs_to :presentation_file, Claper.Presentations.PresentationFile 30 | has_many :poll_opts, Claper.Polls.PollOpt, on_replace: :delete 31 | has_many :poll_votes, Claper.Polls.PollVote, on_replace: :delete 32 | 33 | timestamps() 34 | end 35 | 36 | @doc false 37 | def changeset(poll, attrs) do 38 | poll 39 | |> cast(attrs, [ 40 | :title, 41 | :presentation_file_id, 42 | :position, 43 | :enabled, 44 | :total, 45 | :multiple, 46 | :show_results 47 | ]) 48 | |> cast_assoc(:poll_opts, required: true) 49 | |> validate_required([:title, :presentation_file_id, :position]) 50 | |> validate_length(:title, max: 255) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/claper/polls/poll_opt.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Polls.PollOpt do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | content: String.t(), 8 | vote_count: integer(), 9 | percentage: float(), 10 | poll_id: integer(), 11 | poll: Claper.Polls.Poll.t(), 12 | poll_votes: [Claper.Polls.PollVote.t()], 13 | inserted_at: NaiveDateTime.t(), 14 | updated_at: NaiveDateTime.t() 15 | } 16 | 17 | @derive {Jason.Encoder, only: [:content, :vote_count]} 18 | schema "poll_opts" do 19 | field :content, :string 20 | field :vote_count, :integer 21 | field :percentage, :float, virtual: true 22 | 23 | belongs_to :poll, Claper.Polls.Poll 24 | has_many :poll_votes, Claper.Polls.PollVote, on_replace: :delete 25 | 26 | timestamps() 27 | end 28 | 29 | @doc false 30 | def changeset(poll_opt, attrs) do 31 | poll_opt 32 | |> cast(attrs, [:content, :vote_count, :poll_id]) 33 | |> validate_required([:content]) 34 | |> validate_length(:content, max: 255) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/claper/polls/poll_vote.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Polls.PollVote do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | attendee_identifier: String.t() | nil, 8 | poll_id: integer() | nil, 9 | poll_opt_id: integer() | nil, 10 | user_id: integer() | nil, 11 | inserted_at: NaiveDateTime.t(), 12 | updated_at: NaiveDateTime.t() 13 | } 14 | 15 | schema "poll_votes" do 16 | field :attendee_identifier, :string 17 | 18 | belongs_to :poll, Claper.Polls.Poll 19 | belongs_to :poll_opt, Claper.Polls.PollOpt 20 | belongs_to :user, Claper.Accounts.User 21 | 22 | timestamps() 23 | end 24 | 25 | @doc false 26 | def changeset(poll_vote, attrs) do 27 | poll_vote 28 | |> cast(attrs, [:attendee_identifier, :user_id, :poll_opt_id, :poll_id]) 29 | |> validate_required([:poll_opt_id, :poll_id]) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/claper/posts/post.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Posts.Post do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | body: String.t(), 8 | uuid: Ecto.UUID.t(), 9 | like_count: integer() | nil, 10 | love_count: integer() | nil, 11 | lol_count: integer() | nil, 12 | name: String.t() | nil, 13 | attendee_identifier: String.t() | nil, 14 | position: integer() | nil, 15 | pinned: boolean() | nil, 16 | event_id: integer() | nil, 17 | user_id: integer() | nil, 18 | reactions: [Claper.Posts.Reaction.t()] | nil, 19 | inserted_at: NaiveDateTime.t(), 20 | updated_at: NaiveDateTime.t() 21 | } 22 | 23 | schema "posts" do 24 | field :body, :string 25 | field :uuid, :binary_id 26 | field :like_count, :integer, default: 0 27 | field :love_count, :integer, default: 0 28 | field :lol_count, :integer, default: 0 29 | field :name, :string 30 | field :attendee_identifier, :string 31 | field :position, :integer, default: 0 32 | field :pinned, :boolean, default: false 33 | 34 | belongs_to :event, Claper.Events.Event 35 | belongs_to :user, Claper.Accounts.User 36 | has_many :reactions, Claper.Posts.Reaction 37 | 38 | timestamps() 39 | end 40 | 41 | @doc false 42 | def changeset(post, attrs) do 43 | post 44 | |> cast(attrs, [ 45 | :body, 46 | :attendee_identifier, 47 | :user_id, 48 | :like_count, 49 | :love_count, 50 | :lol_count, 51 | :name, 52 | :position, 53 | :pinned 54 | ]) 55 | |> validate_required([:body, :position]) 56 | |> validate_length(:body, min: 2, max: 255) 57 | end 58 | 59 | def nickname_changeset(post, attrs) do 60 | post 61 | |> cast(attrs, [:name]) 62 | |> validate_required([:name]) 63 | |> validate_length(:name, min: 2, max: 20) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/claper/posts/reaction.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Posts.Reaction do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | icon: String.t() | nil, 8 | attendee_identifier: String.t() | nil, 9 | post_id: integer() | nil, 10 | user_id: integer() | nil, 11 | inserted_at: NaiveDateTime.t(), 12 | updated_at: NaiveDateTime.t() 13 | } 14 | 15 | schema "reactions" do 16 | field :icon, :string 17 | field :attendee_identifier, :string 18 | 19 | belongs_to :post, Claper.Posts.Post 20 | belongs_to :user, Claper.Accounts.User 21 | 22 | timestamps() 23 | end 24 | 25 | @doc false 26 | def changeset(reaction, attrs) do 27 | reaction 28 | |> cast(attrs, [:icon, :attendee_identifier, :user_id, :post_id]) 29 | |> validate_required([:icon, :post_id]) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/claper/presentations/presentation_file.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Presentations.PresentationFile do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | hash: String.t() | nil, 8 | length: integer() | nil, 9 | status: String.t() | nil, 10 | event_id: integer() | nil, 11 | polls: [Claper.Polls.Poll.t()] | nil, 12 | forms: [Claper.Forms.Form.t()] | nil, 13 | embeds: [Claper.Embeds.Embed.t()] | nil, 14 | quizzes: [Claper.Quizzes.Quiz.t()] | nil, 15 | presentation_state: Claper.Presentations.PresentationState.t(), 16 | inserted_at: NaiveDateTime.t(), 17 | updated_at: NaiveDateTime.t() 18 | } 19 | 20 | schema "presentation_files" do 21 | field :hash, :string 22 | field :length, :integer 23 | field :status, :string 24 | 25 | belongs_to :event, Claper.Events.Event 26 | has_many :polls, Claper.Polls.Poll 27 | has_many :forms, Claper.Forms.Form 28 | has_many :embeds, Claper.Embeds.Embed 29 | has_many :quizzes, Claper.Quizzes.Quiz 30 | has_one :presentation_state, Claper.Presentations.PresentationState, on_replace: :delete 31 | 32 | timestamps() 33 | end 34 | 35 | @doc false 36 | def changeset(presentation_file, attrs) do 37 | presentation_file 38 | |> cast(attrs, [:length, :status, :hash, :event_id]) 39 | |> cast_assoc(:presentation_state) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/claper/presentations/presentation_state.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Presentations.PresentationState do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | position: integer() | nil, 8 | chat_visible: boolean() | nil, 9 | poll_visible: boolean() | nil, 10 | join_screen_visible: boolean() | nil, 11 | chat_enabled: boolean() | nil, 12 | anonymous_chat_enabled: boolean() | nil, 13 | message_reaction_enabled: boolean() | nil, 14 | banned: [String.t()] | nil, 15 | show_only_pinned: boolean() | nil, 16 | presentation_file_id: integer() | nil, 17 | inserted_at: NaiveDateTime.t(), 18 | updated_at: NaiveDateTime.t() 19 | } 20 | 21 | schema "presentation_states" do 22 | field :position, :integer 23 | field :chat_visible, :boolean 24 | field :poll_visible, :boolean 25 | field :join_screen_visible, :boolean 26 | field :chat_enabled, :boolean 27 | field :anonymous_chat_enabled, :boolean 28 | field :message_reaction_enabled, :boolean, default: true 29 | field :banned, {:array, :string}, default: [] 30 | field :show_only_pinned, :boolean, default: false 31 | 32 | belongs_to :presentation_file, Claper.Presentations.PresentationFile 33 | 34 | timestamps() 35 | end 36 | 37 | @doc false 38 | def changeset(presentation_state, attrs) do 39 | presentation_state 40 | |> cast(attrs, [ 41 | :position, 42 | :chat_visible, 43 | :poll_visible, 44 | :join_screen_visible, 45 | :banned, 46 | :presentation_file_id, 47 | :chat_enabled, 48 | :anonymous_chat_enabled, 49 | :show_only_pinned, 50 | :message_reaction_enabled 51 | ]) 52 | |> validate_required([]) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/claper/quizzes/quiz.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Quizzes.Quiz do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "quizzes" do 6 | field :title, :string 7 | field :position, :integer, default: 0 8 | field :enabled, :boolean, default: false 9 | field :show_results, :boolean, default: true 10 | field :allow_anonymous, :boolean, default: false 11 | field :lti_line_item_url, :string 12 | 13 | belongs_to :presentation_file, Claper.Presentations.PresentationFile 14 | belongs_to :lti_resource, Lti13.Resources.Resource 15 | 16 | has_many :quiz_questions, Claper.Quizzes.QuizQuestion, 17 | preload_order: [asc: :id], 18 | on_replace: :delete 19 | 20 | has_many :quiz_responses, Claper.Quizzes.QuizResponse 21 | 22 | timestamps() 23 | end 24 | 25 | @doc false 26 | def changeset(quiz, attrs) do 27 | quiz 28 | |> cast(attrs, [ 29 | :title, 30 | :position, 31 | :presentation_file_id, 32 | :enabled, 33 | :show_results, 34 | :allow_anonymous, 35 | :lti_resource_id, 36 | :lti_line_item_url 37 | ]) 38 | |> validate_required([:title, :position, :presentation_file_id]) 39 | |> cast_assoc(:quiz_questions, 40 | required: true, 41 | with: &Claper.Quizzes.QuizQuestion.changeset/2, 42 | sort_param: :quiz_questions_order, 43 | drop_param: :quiz_questions_delete 44 | ) 45 | end 46 | 47 | def update_line_item_changeset(quiz, attrs) do 48 | quiz 49 | |> cast(attrs, [:lti_line_item_url]) 50 | |> validate_required([:lti_line_item_url]) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/claper/quizzes/quiz_question.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Quizzes.QuizQuestion do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | import ClaperWeb.Gettext 6 | 7 | schema "quiz_questions" do 8 | field :content, :string 9 | field :type, :string, default: "qcm" 10 | 11 | belongs_to :quiz, Claper.Quizzes.Quiz 12 | 13 | has_many :quiz_question_opts, Claper.Quizzes.QuizQuestionOpt, 14 | preload_order: [asc: :id], 15 | on_replace: :delete 16 | 17 | timestamps() 18 | end 19 | 20 | @doc false 21 | def changeset(quiz_question, attrs) do 22 | quiz_question 23 | |> cast(attrs, [:content, :type]) 24 | |> validate_required([:content, :type]) 25 | |> cast_assoc(:quiz_question_opts, 26 | required: true, 27 | with: &Claper.Quizzes.QuizQuestionOpt.changeset/2, 28 | sort_param: :quiz_question_opts_order, 29 | drop_param: :quiz_question_opts_delete 30 | ) 31 | |> validate_at_least_one_correct_opt() 32 | end 33 | 34 | defp validate_at_least_one_correct_opt(changeset) do 35 | quiz_question_opts = get_field(changeset, :quiz_question_opts) || [] 36 | has_correct_opt = Enum.any?(quiz_question_opts, & &1.is_correct) 37 | 38 | if has_correct_opt do 39 | changeset 40 | else 41 | add_error(changeset, :quiz_question_opts, gettext("must have at least one correct answer")) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/claper/quizzes/quiz_question_opt.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Quizzes.QuizQuestionOpt do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "quiz_question_opts" do 6 | field :content, :string 7 | field :is_correct, :boolean, default: false 8 | field :response_count, :integer, default: 0 9 | field :percentage, :float, virtual: true 10 | 11 | belongs_to :quiz_question, Claper.Quizzes.QuizQuestion 12 | 13 | timestamps() 14 | end 15 | 16 | @doc false 17 | def changeset(quiz_question_opt, attrs) do 18 | quiz_question_opt 19 | |> cast(attrs, [:content, :is_correct, :response_count]) 20 | |> validate_required([:content, :is_correct]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/claper/quizzes/quiz_response.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Quizzes.QuizResponse do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "quiz_responses" do 6 | field :attendee_identifier, :string 7 | 8 | belongs_to :quiz, Claper.Quizzes.Quiz 9 | belongs_to :quiz_question, Claper.Quizzes.QuizQuestion 10 | belongs_to :quiz_question_opt, Claper.Quizzes.QuizQuestionOpt 11 | belongs_to :user, Claper.Accounts.User 12 | 13 | timestamps() 14 | end 15 | 16 | @doc false 17 | def changeset(quiz_response, attrs) do 18 | quiz_response 19 | |> cast(attrs, [ 20 | :attendee_identifier, 21 | :quiz_id, 22 | :quiz_question_id, 23 | :quiz_question_opt_id, 24 | :user_id 25 | ]) 26 | |> validate_required([:quiz_id, :quiz_question_id, :quiz_question_opt_id]) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/claper/release.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Release do 2 | @moduledoc """ 3 | Used for executing DB release tasks when run in production without Mix 4 | installed. 5 | """ 6 | @app :claper 7 | 8 | def migrate do 9 | load_app() 10 | 11 | Application.ensure_all_started(:ssl) 12 | 13 | for repo <- repos() do 14 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) 15 | end 16 | end 17 | 18 | def seeds do 19 | load_app() 20 | 21 | for repo <- repos() do 22 | {:ok, _, _} = 23 | Ecto.Migrator.with_repo(repo, fn _repo -> 24 | Code.eval_file("priv/repo/seeds.exs") 25 | end) 26 | end 27 | end 28 | 29 | def rollback(repo, version) do 30 | load_app() 31 | {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) 32 | end 33 | 34 | defp repos do 35 | Application.fetch_env!(@app, :ecto_repos) 36 | end 37 | 38 | defp load_app do 39 | Application.load(@app) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/claper/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo do 2 | use Ecto.Repo, 3 | otp_app: :claper, 4 | adapter: Ecto.Adapters.Postgres 5 | 6 | import Ecto.Query 7 | 8 | @default_page_size 12 9 | 10 | def init(_type, config) do 11 | if url = System.get_env("DATABASE_URL") do 12 | {:ok, Keyword.put(config, :url, url)} 13 | else 14 | {:ok, config} 15 | end 16 | end 17 | 18 | def paginate(query, opts \\ []) do 19 | page = Keyword.get(opts, :page, 1) 20 | page_size = Keyword.get(opts, :page_size, @default_page_size) 21 | preload = Keyword.get(opts, :preload, []) 22 | 23 | total_entries = 24 | query 25 | |> exclude(:order_by) 26 | |> exclude(:preload) 27 | |> exclude(:select) 28 | |> select(count("*")) 29 | |> Claper.Repo.one() 30 | 31 | total_pages = ceil(total_entries / page_size) 32 | 33 | results = 34 | query 35 | |> limit(^page_size) 36 | |> offset(^((page - 1) * page_size)) 37 | |> Claper.Repo.all() 38 | |> Claper.Repo.preload(preload) 39 | 40 | {results, total_entries, total_pages} 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/claper/schema.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Schema do 2 | defmacro __using__(_) do 3 | quote do 4 | use Ecto.Schema 5 | @primary_key {:id, :binary_id, autogenerate: false} 6 | @foreign_key_type :binary_id 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/claper/stats/stat.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.Stats.Stat do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | attendee_identifier: String.t() | nil, 8 | event_id: integer() | nil, 9 | user_id: integer() | nil, 10 | inserted_at: NaiveDateTime.t(), 11 | updated_at: NaiveDateTime.t() 12 | } 13 | 14 | schema "stats" do 15 | field :attendee_identifier, :string 16 | 17 | belongs_to :event, Claper.Events.Event 18 | belongs_to :user, Claper.Accounts.User 19 | 20 | timestamps() 21 | end 22 | 23 | @doc false 24 | def changeset(stat, attrs) do 25 | stat 26 | |> cast(attrs, [ 27 | :attendee_identifier, 28 | :event_id, 29 | :user_id 30 | ]) 31 | |> cast_assoc(:event) 32 | |> unique_constraint([:event_id, :user_id]) 33 | |> unique_constraint([:event_id, :attendee_identifier]) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/claper_web/channels/presence.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.Presence do 2 | @moduledoc """ 3 | Provides presence tracking to channels and processes. 4 | 5 | See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html) 6 | docs for more details. 7 | """ 8 | use Phoenix.Presence, 9 | otp_app: :claper, 10 | pubsub_server: Claper.PubSub 11 | end 12 | -------------------------------------------------------------------------------- /lib/claper_web/controllers/event_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.EventController do 2 | use ClaperWeb, :controller 3 | 4 | def attendee_identifier(conn, _opts) do 5 | conn |> set_token() 6 | end 7 | 8 | defp set_token(conn) do 9 | if is_nil(get_session(conn, :attendee_identifier)) do 10 | token = Base.url_encode64(:crypto.strong_rand_bytes(8)) 11 | 12 | conn 13 | |> put_session(:attendee_identifier, token) 14 | else 15 | conn 16 | end 17 | end 18 | 19 | def slide_generate(conn, %{"uuid" => uuid, "qr" => qr} = _opts) do 20 | with event <- Claper.Events.get_event!(uuid) do 21 | "data:image/png;base64," <> raw = qr 22 | {:ok, data} = Base.decode64(raw) 23 | dir = System.tmp_dir!() 24 | tmp_file = Path.join(dir, "qr-#{uuid}.png") 25 | File.write!(tmp_file, data, [:binary]) 26 | 27 | code = String.upcase(event.code) 28 | 29 | {output, 0} = 30 | System.cmd("convert", [ 31 | "-size", 32 | "1920x1080", 33 | "xc:black", 34 | "-fill", 35 | "white", 36 | "-font", 37 | "Roboto", 38 | "-pointsize", 39 | "45", 40 | "-gravity", 41 | "north", 42 | "-annotate", 43 | "+0+100", 44 | "Scannez pour interagir en temps-réel", 45 | "-gravity", 46 | "center", 47 | "-annotate", 48 | "+0+200", 49 | "Ou utilisez le code:", 50 | "-pointsize", 51 | "65", 52 | "-gravity", 53 | "center", 54 | "-annotate", 55 | "+0+350", 56 | "##{code}", 57 | tmp_file, 58 | "-gravity", 59 | "north", 60 | "-geometry", 61 | "+0+230", 62 | "-composite", 63 | "jpg:-" 64 | ]) 65 | 66 | conn 67 | |> put_resp_content_type("image/png") 68 | |> send_resp(200, output) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/claper_web/controllers/mailbox_guard.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.MailboxGuard do 2 | import Plug.Conn 3 | import Phoenix.Controller 4 | 5 | def init(default), do: default 6 | 7 | def call(conn, _params \\ %{}) do 8 | mailbox_username = 9 | Application.get_env(:claper, ClaperWeb.MailboxGuard) |> Keyword.get(:username) 10 | 11 | mailbox_password = 12 | Application.get_env(:claper, ClaperWeb.MailboxGuard) |> Keyword.get(:password) 13 | 14 | mailbox_enabled = 15 | Application.get_env(:claper, ClaperWeb.MailboxGuard) |> Keyword.get(:enabled) 16 | 17 | IO.puts(mailbox_enabled) 18 | 19 | if mailbox_enabled do 20 | if mailbox_username && mailbox_password do 21 | Plug.BasicAuth.basic_auth(conn, username: mailbox_username, password: mailbox_password) 22 | else 23 | conn 24 | end 25 | else 26 | conn |> redirect(to: "/") |> halt() 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/claper_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.PageController do 2 | use ClaperWeb, :controller 3 | 4 | def index(conn, _params) do 5 | conn 6 | |> render("index.html") 7 | end 8 | 9 | def tos(conn, _params) do 10 | conn 11 | |> render("tos.html") 12 | end 13 | 14 | def privacy(conn, _params) do 15 | conn 16 | |> render("privacy.html") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/claper_web/controllers/post_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.PostController do 2 | use ClaperWeb, :controller 3 | 4 | def index(conn, %{"event_id" => event_id}) do 5 | try do 6 | with event <- Claper.Events.get_event!(event_id), 7 | posts <- Claper.Posts.list_posts(event.uuid, [:user, :attendee]) do 8 | render(conn, "index.json", posts: posts) 9 | end 10 | rescue 11 | Ecto.NoResultsError -> 12 | conn 13 | |> put_status(:not_found) 14 | |> put_view(ClaperWeb.ErrorView) 15 | |> render(:"404") 16 | end 17 | end 18 | 19 | def create(conn, %{"event_id" => event_id, "body" => body}) do 20 | try do 21 | with event <- Claper.Events.get_event!(event_id) do 22 | case Claper.Posts.create_post(event, %{body: body}) do 23 | {:ok, post} -> render(conn, "post.json", post: post) 24 | end 25 | end 26 | rescue 27 | Ecto.NoResultsError -> 28 | conn 29 | |> put_status(:not_found) 30 | |> put_view(ClaperWeb.ErrorView) 31 | |> render(:"404") 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/claper_web/controllers/user_registration_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserRegistrationController do 2 | use ClaperWeb, :controller 3 | 4 | alias Claper.Accounts 5 | alias Claper.Accounts.User 6 | alias ClaperWeb.UserAuth 7 | 8 | def new(conn, _params) do 9 | if Application.get_env(:claper, :enable_account_creation) do 10 | changeset = Accounts.change_user_registration(%User{}) 11 | render(conn, "new.html", changeset: changeset) 12 | else 13 | conn 14 | |> put_flash(:error, gettext("Account creation is disabled")) 15 | |> redirect(to: "/") 16 | end 17 | end 18 | 19 | def confirm(conn, _params) do 20 | render(conn, "confirm.html") 21 | end 22 | 23 | def create(conn, %{"user" => user_params}) do 24 | case Accounts.register_user(user_params(user_params)) do 25 | {:ok, user} -> 26 | if Application.get_env(:claper, :email_confirmation) do 27 | {:ok, _} = 28 | Accounts.deliver_user_confirmation_instructions( 29 | user, 30 | &url(~p"/users/confirm/#{&1}") 31 | ) 32 | 33 | conn 34 | |> redirect(to: ~p"/users/register/confirm") 35 | else 36 | conn 37 | |> put_flash(:info, "User created successfully.") 38 | |> UserAuth.log_in_user(user) 39 | end 40 | 41 | {:error, %Ecto.Changeset{} = changeset} -> 42 | render(conn, "new.html", changeset: changeset) 43 | end 44 | end 45 | 46 | def delete(conn, _params) do 47 | Accounts.delete_user(conn.assigns.current_user) 48 | 49 | conn 50 | |> put_flash(:info, gettext("Your account has been deleted.")) 51 | |> UserAuth.log_out_user() 52 | end 53 | 54 | defp user_params(params) do 55 | if Application.get_env(:claper, :email_confirmation) do 56 | params 57 | else 58 | params 59 | |> Map.put("confirmed_at", NaiveDateTime.utc_now()) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/claper_web/controllers/user_settings_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserSettingsController do 2 | use ClaperWeb, :controller 3 | 4 | alias Claper.Accounts 5 | 6 | plug :assign_email_and_password_changesets 7 | 8 | def edit(conn, _params) do 9 | render(conn, "edit.html") 10 | end 11 | 12 | def update(conn, %{"action" => "update_email"} = params) do 13 | %{"user" => user_params} = params 14 | user = conn.assigns.current_user 15 | 16 | case Accounts.apply_user_email(user, user_params) do 17 | {:ok, applied_user} -> 18 | Accounts.deliver_update_email_instructions( 19 | applied_user, 20 | user.email, 21 | &url(~p"/users/settings/confirm_email/#{&1}") 22 | ) 23 | 24 | conn 25 | |> put_flash( 26 | :info, 27 | "A link to confirm your email change has been sent to the new address." 28 | ) 29 | |> redirect(to: ~p"/users/settings") 30 | 31 | {:error, changeset} -> 32 | render(conn, "edit.html", email_changeset: changeset) 33 | end 34 | end 35 | 36 | def confirm_email(conn, %{"token" => token}) do 37 | case Accounts.update_user_email(conn.assigns.current_user, token) do 38 | :ok -> 39 | conn 40 | |> put_flash(:info, "Email changed successfully.") 41 | |> redirect(to: ~p"/users/settings") 42 | 43 | :error -> 44 | conn 45 | |> put_flash(:error, "Email change link is invalid or it has expired.") 46 | |> redirect(to: ~p"/users/settings") 47 | end 48 | end 49 | 50 | defp assign_email_and_password_changesets(conn, _opts) do 51 | user = conn.assigns.current_user 52 | 53 | conn 54 | |> assign(:email_changeset, Accounts.change_user_email(user)) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/claper_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import ClaperWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :claper 24 | end 25 | -------------------------------------------------------------------------------- /lib/claper_web/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.Helpers do 2 | def format_body(body) do 3 | url_regex = ~r/(https?:\/\/[^\s]+)/ 4 | 5 | body 6 | |> String.split(url_regex, include_captures: true) 7 | |> Enum.map(fn 8 | "http" <> _rest = url -> 9 | Phoenix.HTML.raw( 10 | ~s(#{url}) 11 | ) 12 | 13 | text -> 14 | text 15 | end) 16 | end 17 | 18 | def body_without_links(text) do 19 | url_regex = ~r/(https?:\/\/[^\s]+)/ 20 | String.replace(text, url_regex, "") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/claper_web/live/attendee_live_auth.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.AttendeeLiveAuth do 2 | import Phoenix.Component 3 | 4 | def on_mount(:default, _params, session, socket) do 5 | socket = 6 | socket 7 | |> assign(:attendee_identifier, session["attendee_identifier"]) 8 | |> assign(:current_user, session["current_user"]) 9 | 10 | {:cont, socket} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/claper_web/live/event_live/embed_iframe_component.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.EventLive.EmbedIframeComponent do 2 | use ClaperWeb, :live_component 3 | 4 | def render(assigns) do 5 | ~H""" 6 |
7 | <%= case @provider do %> 8 | <% "youtube" -> %> 9 | 17 | <% "vimeo" -> %> 18 | 25 | <% "canva" -> %> 26 | 33 | <% "googleslides" -> %> 34 | 41 | <% "custom" -> %> 42 | <%= raw(@content) %> 43 | <% end %> 44 |
45 | """ 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/claper_web/live/event_live/join.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.EventLive.Join do 2 | use ClaperWeb, :live_view 3 | 4 | on_mount(ClaperWeb.AttendeeLiveAuth) 5 | 6 | @impl true 7 | def mount(params, session, socket) do 8 | with %{"locale" => locale} <- session do 9 | Gettext.put_locale(ClaperWeb.Gettext, locale) 10 | end 11 | 12 | if params["disconnected_from"] do 13 | try do 14 | event = Claper.Events.get_event!(params["disconnected_from"]) 15 | {:ok, socket |> assign(:last_event, event)} 16 | rescue 17 | _ -> {:ok, socket |> assign(:last_event, nil)} 18 | end 19 | else 20 | {:ok, socket |> assign(:last_event, nil)} 21 | end 22 | end 23 | 24 | @impl true 25 | def handle_params(params, _url, socket) do 26 | {:noreply, apply_action(socket, socket.assigns.live_action, params)} 27 | end 28 | 29 | @impl true 30 | def handle_event("join", %{"event" => %{"code" => code}}, socket) do 31 | {:noreply, socket |> push_navigate(to: ~p"/e/#{String.downcase(code)}")} 32 | end 33 | 34 | defp apply_action(socket, :join, _params) do 35 | socket 36 | |> redirect(to: "/") 37 | end 38 | 39 | defp apply_action(socket, :index, _params) do 40 | socket 41 | |> assign(:page_title, gettext("Join")) 42 | |> assign(:event, nil) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/claper_web/live/live_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.LiveHelpers do 2 | end 3 | -------------------------------------------------------------------------------- /lib/claper_web/live/user_live_auth.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserLiveAuth do 2 | import Phoenix.LiveView 3 | import Phoenix.Component 4 | 5 | use Phoenix.VerifiedRoutes, 6 | endpoint: ClaperWeb.Endpoint, 7 | router: ClaperWeb.Router 8 | 9 | def on_mount(:default, _params, %{"current_user" => current_user} = _session, socket) do 10 | socket = assign_new(socket, :current_user, fn -> current_user end) 11 | 12 | cond do 13 | not Application.get_env(:claper, :email_confirmation) -> 14 | {:cont, socket} 15 | 16 | current_user.confirmed_at -> 17 | {:cont, socket} 18 | 19 | true -> 20 | {:halt, redirect(socket, to: ~p"/users/register/confirm")} 21 | end 22 | end 23 | 24 | def on_mount(:default, _params, _session, socket), 25 | do: {:halt, redirect(socket, to: ~p"/users/register/confirm")} 26 | end 27 | -------------------------------------------------------------------------------- /lib/claper_web/notifiers/leader_notifier.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.Notifiers.LeaderNotifier do 2 | use Phoenix.Swoosh, view: ClaperWeb.LeaderNotifierView, layout: {ClaperWeb.LayoutView, :email} 3 | import ClaperWeb.Gettext 4 | 5 | def event_invitation(event_name, email, url) do 6 | new() 7 | |> to(email) 8 | |> from( 9 | {Application.get_env(:claper, :mail) |> Keyword.get(:from_name), 10 | Application.get_env(:claper, :mail) |> Keyword.get(:from)} 11 | ) 12 | |> subject(gettext("You have been invited to manage an event")) 13 | |> render_body("invitation.html", %{event_name: event_name, leader_email: email, url: url}) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/claper_web/plugs/iframe.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.Plugs.Iframe do 2 | import Plug.Conn 3 | def init(_), do: %{} 4 | 5 | def call(conn, _opts) do 6 | conn 7 | |> put_resp_header( 8 | "x-frame-options", 9 | "ALLOWALL" 10 | ) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/claper_web/templates/error/404.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= csrf_meta_tag() %> 8 | Not found - Claper 9 | 10 | 11 | 12 | 14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |

26 | 27 | 28 | 29 |

30 |

31 | <%= gettext("Oops, page doesn't exist.") %> 32 |

33 | 34 | 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /lib/claper_web/templates/error/csrf_error.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | <%= gettext("CSRF Verification Failed") %> 6 |

7 |

8 | <%= gettext("A required security token was not found or was invalid.") %> 9 |

10 |
11 | 12 |
13 |

14 | <%= gettext("If you're continually seeing this issue, try the following:") %> 15 |

16 |
    17 |
  1. <%= gettext("Clear cookies (at least for Claper domain)") %>
  2. 18 |
  3. <%= gettext("Reload the page you're trying to access (don't re-submit data)") %>
  4. 19 |
  5. <%= gettext("Try logging in again") %>
  6. 20 |
  7. <%= gettext("Ensure the URL does not contain an extra \"/\" anywhere") %>
  8. 21 |
22 |

23 | <%= gettext("If the problem persists, please contact support.") %> 24 |

25 |
26 | <%= link(gettext("Back to Login"), 27 | to: ~p"/users/log_in", 28 | class: "text-blue underline font-semibold transition duration-300 ease-in-out" 29 | ) %> 30 |
31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /lib/claper_web/templates/layout/_avatar.html.heex: -------------------------------------------------------------------------------- 1 |
2 | <%= if @user.avatar do %> 3 | avatar 4 | <% else %> 5 |
6 | <%= if @user.full_name do %> 7 | <%= with [first | _] <- String.codepoints(@user.full_name), do: String.capitalize(first) %> 8 | <% else %> 9 | <%= with [first | _] <- String.codepoints(@user.email), do: String.capitalize(first) %> 10 | <% end %> 11 |
12 | <% end %> 13 |
14 | -------------------------------------------------------------------------------- /lib/claper_web/templates/layout/_user_menu.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 6 | <%= gettext("My account") %> 7 | 8 | 12 | <%= gettext("Documentation") %> 13 | 14 |
15 |
16 | <%= link(gettext("Logout"), 17 | to: ~p"/users/log_out", 18 | method: :delete, 19 | class: "text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900" 20 | ) %> 21 |
22 |
23 |

24 | Version <%= Application.spec(:claper, :vsn) %> 25 |

26 |
27 | -------------------------------------------------------------------------------- /lib/claper_web/templates/layout/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 | <%= if Phoenix.Flash.get(@flash, :info) do %> 3 | 4 | <% end %> 5 | 6 | <%= if Phoenix.Flash.get(@flash, :error) do %> 7 | 8 | <% end %> 9 |
10 | 11 | <%= @inner_content %> 12 | -------------------------------------------------------------------------------- /lib/claper_web/templates/layout/live.html.heex: -------------------------------------------------------------------------------- 1 |
2 | <%= if live_flash(@flash, :info) do %> 3 | 4 | <% end %> 5 | 6 | <%= if live_flash(@flash, :error) do %> 7 | 8 | <% end %> 9 |
10 | 11 | <%= @inner_content %> 12 | -------------------------------------------------------------------------------- /lib/claper_web/templates/layout/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= csrf_meta_tag() %> 8 | <.live_title suffix=" · Claper" )><%= assigns[:page_title] || "Claper" %> 9 | 10 | 11 | 12 | 14 | 15 | 16 |
17 | 18 |
19 |
20 | <%= @inner_content %> 21 |
22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/claper_web/templates/layout/user.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= csrf_meta_tag() %> 8 | <.live_title suffix=" · Claper" )><%= assigns[:page_title] || "Claper" %> 9 | 10 | 11 | 12 | 14 | 15 | 16 |
17 | 18 |
19 | <%= render("_profile_dropdown.html", user: @current_user, conn: @conn) %> 20 |
21 | <%= @inner_content %> 22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/claper_web/templates/lti/launch/error.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | <%= gettext("Oops") %> 6 |

7 |
8 |

9 | <%= gettext("You cannot perform this action") %> 10 |

11 |
<%= @msg %>
12 |
13 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /lib/claper_web/templates/lti/registration/success.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | <%= gettext("Registration completed") %> 6 | 7 |

8 |
9 |

10 | <%= gettext("Your next steps") %>: 11 |

12 |
13 |
    14 |
  1. <%= gettext("Activate the tool in your LMS") %>
  2. 15 |
  3. <%= gettext("Configure it to be opened in a new window") %>
  4. 16 |
  5. 17 | <%= gettext("Check the permissions to share name and email of users") %> 18 |
  6. 19 |
20 |
21 |
22 | 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /lib/claper_web/templates/page/user_confirmation/edit.html.heex: -------------------------------------------------------------------------------- 1 |

Confirm account

2 | 3 | <.form :let={_f} for={:user} action={~p"/users/confirm/#{@token}"}> 4 |
5 | <%= submit("Confirm my account") %> 6 |
7 | 8 | 9 |

10 | <%= link("Register", to: ~p"/users/register") %> | <%= link("Log in", 11 | to: ~p"/users/log_in" 12 | ) %> 13 |

14 | -------------------------------------------------------------------------------- /lib/claper_web/templates/page/user_confirmation/new.html.heex: -------------------------------------------------------------------------------- 1 |

Resend confirmation instructions

2 | 3 | <.form :let={f} for={:user} action={~p"/users/confirm"}> 4 | <%= label(f, :email) %> 5 | <%= email_input(f, :email, required: true) %> 6 | 7 |
8 | <%= submit("Resend confirmation instructions") %> 9 |
10 | 11 | 12 |

13 | <%= link("Register", to: ~p"/users/register") %> | <%= link("Log in", 14 | to: ~p"/users/log_in" 15 | ) %> 16 |

17 | -------------------------------------------------------------------------------- /lib/claper_web/templates/user_confirmation/new.html.heex: -------------------------------------------------------------------------------- 1 |

Resend confirmation instructions

2 | 3 | <%= form_for :user, ~p"/users/confirm", fn f -> %> 4 | <%= label(f, :email) %> 5 | <%= email_input(f, :email, required: true) %> 6 | 7 |
8 | <%= submit("Resend confirmation instructions") %> 9 |
10 | <% end %> 11 | 12 |

13 | <%= link("Register", to: ~p"/users/register") %> | <%= link("Log in", 14 | to: ~p"/users/log_in" 15 | ) %> 16 |

17 | -------------------------------------------------------------------------------- /lib/claper_web/templates/user_registration/confirm.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

7 | 8 | 9 | 10 |

11 |

12 | <%= if @conn.query_params["retry"] do %> 13 | <%= gettext("We already sent you an email to login, please retry in 5 minutes.") %> 14 | <% else %> 15 | <%= if @conn.query_params["email"] do %> 16 | <%= gettext("We sent you an email at") <> 17 | " #{@conn.query_params["email"]}" <> 18 | gettext(", click on the provided link to connect (check your spam !)") %> 19 | <% else %> 20 | <%= gettext( 21 | "We sent you an email, click on the provided link to connect (check your spam !)" 22 | ) %> 23 | <% end %> 24 | <% end %> 25 |

26 | 27 |
28 | <.link href={~p"/"} class="text-sm text-white underline"> 29 | <%= gettext("back to the home page") %> 30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /lib/claper_web/templates/user_reset_password/new.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

7 | 8 | 9 | 10 |

11 |

12 | <%= gettext("Reset your password") %> 13 |

14 |
15 | 16 | <.form :let={f} for={@changeset} action={~p"/users/reset_password"} class="mt-8 space-y-6"> 17 | <%= if @changeset.action do %> 18 | 22 | <% end %> 23 | 31 |
32 | 38 |
39 | 40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /lib/claper_web/views/attendee_registration_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.AttendeeRegistrationView do 2 | use ClaperWeb, :view 3 | 4 | def render("attendee.json", %{attendee: attendee, token: token}) do 5 | %{ 6 | name: attendee.name, 7 | avatar: attendee.avatar, 8 | token: token 9 | } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/claper_web/views/component_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.ComponentView do 2 | use ClaperWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/claper_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | import Phoenix.HTML.Form 7 | use PhoenixHTMLHelpers 8 | 9 | @doc """ 10 | Generates tag for inlined form input errors. 11 | """ 12 | def error_tag(form, field) do 13 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 14 | content_tag(:span, translate_error(error), 15 | class: "invalid-feedback", 16 | phx_feedback_for: input_name(form, field) 17 | ) 18 | end) 19 | end 20 | 21 | @doc """ 22 | Translates an error message using gettext. 23 | """ 24 | def translate_error({msg, opts}) do 25 | # When using gettext, we typically pass the strings we want 26 | # to translate as a static argument: 27 | # 28 | # # Translate "is invalid" in the "errors" domain 29 | # dgettext("errors", "is invalid") 30 | # 31 | # # Translate the number of files with plural rules 32 | # dngettext("errors", "1 file", "%{count} files", count) 33 | # 34 | # Because the error messages we show in our forms and APIs 35 | # are defined inside Ecto, we need to translate them dynamically. 36 | # This requires us to call the Gettext module passing our gettext 37 | # backend as first argument. 38 | # 39 | # Note we use the "errors" domain, which means translations 40 | # should be written to the errors.po file. The :count option is 41 | # set by Ecto and indicates we should also apply plural rules. 42 | if count = opts[:count] do 43 | Gettext.dngettext(ClaperWeb.Gettext, "errors", msg, msg, count, opts) 44 | else 45 | Gettext.dgettext(ClaperWeb.Gettext, "errors", msg, opts) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/claper_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.ErrorView do 2 | use ClaperWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/claper_web/views/event_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.EventView do 2 | use ClaperWeb, :view 3 | 4 | def render("show.json", %{event: event}) do 5 | %{data: render_one(event, ClaperWeb.EventView, "event.json")} 6 | end 7 | 8 | def render("event.json", %{event: event}) do 9 | %{ 10 | uuid: event.uuid, 11 | name: event.name, 12 | posts: render_many(event.posts, ClaperWeb.PostView, "post.json") 13 | } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/claper_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.LayoutView do 2 | import Phoenix.Component 3 | use ClaperWeb, :view 4 | 5 | # Phoenix LiveDashboard is available only in development by default, 6 | # so we instruct Elixir to not warn if the dashboard route is missing. 7 | @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} 8 | 9 | def active_class(conn, path) do 10 | current_path = Path.join(["/" | conn.path_info]) 11 | 12 | if path == current_path do 13 | "bg-gray-900 text-white" 14 | else 15 | "" 16 | end 17 | end 18 | 19 | def active_live_class(conn, path) do 20 | if path == conn.host_uri do 21 | "bg-gray-900 text-white" 22 | else 23 | "" 24 | end 25 | end 26 | 27 | def active_link(%Plug.Conn{} = conn, text, opts) do 28 | class = 29 | [opts[:class], active_class(conn, opts[:to])] 30 | |> Enum.filter(& &1) 31 | |> Enum.join(" ") 32 | 33 | opts = 34 | opts 35 | |> Keyword.put(:class, class) 36 | 37 | link(text, opts) 38 | end 39 | 40 | def active_link(%Phoenix.LiveView.Socket{} = conn, text, opts) do 41 | class = 42 | [opts[:class], active_live_class(conn, opts[:to])] 43 | |> Enum.filter(& &1) 44 | |> Enum.join(" ") 45 | 46 | opts = 47 | opts 48 | |> Keyword.put(:class, class) 49 | 50 | link(text, opts) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/claper_web/views/leader_notifier_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.LeaderNotifierView do 2 | use Phoenix.View, root: "lib/claper_web/templates" 3 | import ClaperWeb.Gettext 4 | use PhoenixHTMLHelpers 5 | end 6 | -------------------------------------------------------------------------------- /lib/claper_web/views/lti/grade_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.Lti.GradeView do 2 | use ClaperWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/claper_web/views/lti/launch_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.Lti.LaunchView do 2 | use ClaperWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/claper_web/views/lti/registration_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.Lti.RegistrationView do 2 | use ClaperWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/claper_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.PageView do 2 | use ClaperWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/claper_web/views/post_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.PostView do 2 | use ClaperWeb, :view 3 | 4 | def render("index.json", %{posts: posts}) do 5 | %{data: render_many(posts, ClaperWeb.PostView, "post.json")} 6 | end 7 | 8 | def render("post.json", %{post: %{user: %{uuid: _} = user} = post}) do 9 | %{ 10 | uuid: post.uuid, 11 | body: post.body, 12 | inserted_at: post.inserted_at, 13 | user: render_one(user, ClaperWeb.UserView, "user.json") 14 | } 15 | end 16 | 17 | def render("post.json", %{post: %{attendee: %{uuid: _} = attendee} = post}) do 18 | %{ 19 | uuid: post.uuid, 20 | body: post.body, 21 | inserted_at: post.inserted_at, 22 | attendee: render_one(attendee, ClaperWeb.AttendeeView, "attendee.json") 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/claper_web/views/user_confirmation_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserConfirmationView do 2 | use ClaperWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/claper_web/views/user_notifier_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserNotifierView do 2 | use Phoenix.View, root: "lib/claper_web/templates" 3 | import ClaperWeb.Gettext 4 | import Phoenix.HTML 5 | use PhoenixHTMLHelpers 6 | end 7 | -------------------------------------------------------------------------------- /lib/claper_web/views/user_registration_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserRegistrationView do 2 | import Phoenix.Component 3 | use ClaperWeb, :view 4 | 5 | def render("user.json", %{user_registration: user}) do 6 | %{ 7 | email: user.email, 8 | name: user.full_name 9 | } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/claper_web/views/user_reset_password_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserResetPasswordView do 2 | import Phoenix.Component 3 | use ClaperWeb, :view 4 | end 5 | -------------------------------------------------------------------------------- /lib/claper_web/views/user_session_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserSessionView do 2 | import Phoenix.Component 3 | use ClaperWeb, :view 4 | end 5 | -------------------------------------------------------------------------------- /lib/claper_web/views/user_settings_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserSettingsView do 2 | use ClaperWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /lib/claper_web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserView do 2 | use ClaperWeb, :view 3 | 4 | def render("user.json", %{user: user}) do 5 | %{ 6 | uuid: user.uuid, 7 | email: user.email 8 | } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/lti_13.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13 do 2 | end 3 | -------------------------------------------------------------------------------- /lib/lti_13/deployments.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Deployments do 2 | import Ecto.Query, warn: false 3 | alias Claper.Repo 4 | alias Lti13.Deployments.Deployment 5 | 6 | @doc """ 7 | Creates a deployment. 8 | 9 | ## Examples 10 | iex> create_deployment(%{deployment_id: 1, registration_id: 1}) 11 | {:ok, %Deployment{}} 12 | iex> create_deployment(%{deployment_id: :bad_value, registration_id: 1}) 13 | {:error, %Ecto.Changeset{}} 14 | """ 15 | def create_deployment(attrs) do 16 | %Deployment{} 17 | |> Deployment.changeset(attrs) 18 | |> Repo.insert() 19 | end 20 | 21 | @doc """ 22 | Gets a deployment by registration and deployment id. 23 | 24 | ## Examples 25 | iex> get_deployment(1, 1) 26 | %Deployment{} 27 | iex> get_deployment(1, :bad_value) 28 | nil 29 | """ 30 | def get_deployment(registration_id, deployment_id) do 31 | Repo.one( 32 | from(r in Deployment, 33 | where: r.registration_id == ^registration_id and r.deployment_id == ^deployment_id 34 | ) 35 | ) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/lti_13/deployments/deployment.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Deployments.Deployment do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "lti_13_deployments" do 6 | field :deployment_id, :integer 7 | 8 | belongs_to :registration, Lti13.Registrations.Registration 9 | 10 | timestamps() 11 | end 12 | 13 | @doc false 14 | def changeset(deployment, attrs \\ %{}) do 15 | deployment 16 | |> cast(attrs, [:deployment_id, :registration_id]) 17 | |> validate_required([:deployment_id, :registration_id]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/lti_13/jwks.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Jwks do 2 | import Ecto.Query, warn: false 3 | alias Claper.Repo 4 | 5 | alias Lti13.Jwks.Jwk 6 | 7 | def create_jwk(attrs) do 8 | %Jwk{} 9 | |> Jwk.changeset(attrs) 10 | |> Repo.insert() 11 | end 12 | 13 | def get_active_jwk() do 14 | case Repo.all(from(k in Jwk, where: k.active == true, order_by: [desc: k.id], limit: 1)) do 15 | [head | _] -> head 16 | _ -> {:error, %{msg: "No active Jwk found", reason: :not_found}} 17 | end 18 | end 19 | 20 | def get_all_jwks() do 21 | Repo.all(from(k in Jwk)) 22 | end 23 | 24 | def get_jwk_by_registration(%Lti13.Registrations.Registration{tool_jwk_id: tool_jwk_id}) do 25 | Repo.one( 26 | from(jwk in Jwk, 27 | where: jwk.id == ^tool_jwk_id 28 | ) 29 | ) 30 | end 31 | 32 | @doc """ 33 | Gets a all public keys. 34 | ## Examples 35 | iex> get_all_public_keys() 36 | %{keys: []} 37 | """ 38 | def get_all_public_keys() do 39 | public_keys = 40 | get_all_jwks() 41 | |> Enum.map(fn %{pem: pem, typ: typ, alg: alg, kid: kid} -> 42 | pem 43 | |> JOSE.JWK.from_pem() 44 | |> JOSE.JWK.to_public() 45 | |> JOSE.JWK.to_map() 46 | |> (fn {_kty, public_jwk} -> public_jwk end).() 47 | |> Map.put("typ", typ) 48 | |> Map.put("alg", alg) 49 | |> Map.put("kid", kid) 50 | |> Map.put("use", "sig") 51 | end) 52 | 53 | %{keys: public_keys} 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/lti_13/jwks/jwk.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Jwks.Jwk do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "lti_13_jwks" do 6 | field :pem, :string 7 | field :typ, :string 8 | field :alg, :string 9 | field :kid, :string 10 | field :active, :boolean, default: false 11 | 12 | has_many :registrations, Lti13.Registrations.Registration, foreign_key: :tool_jwk_id 13 | 14 | timestamps() 15 | end 16 | 17 | @doc false 18 | def changeset(jwk, attrs \\ %{}) do 19 | jwk 20 | |> cast(attrs, [:pem, :typ, :alg, :kid, :active]) 21 | |> validate_required([:pem, :typ, :alg, :kid]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/lti_13/nonces.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Nonces do 2 | import Ecto.Query, warn: false 3 | alias Claper.Repo 4 | alias Lti13.Nonces.Nonce 5 | 6 | def get_nonce(value, domain \\ nil) do 7 | case domain do 8 | nil -> 9 | Repo.get_by(Nonce, value: value) 10 | 11 | domain -> 12 | Repo.get_by(Nonce, value: value, domain: domain) 13 | end 14 | end 15 | 16 | def create_nonce(attrs) do 17 | %Nonce{} 18 | |> Nonce.changeset(attrs) 19 | |> Repo.insert() 20 | end 21 | 22 | # 86400 seconds = 24 hours 23 | def delete_expired_nonces(nonce_ttl_sec \\ 86_400) do 24 | nonce_expiry = DateTime.utc_now() |> DateTime.add(-nonce_ttl_sec, :second) 25 | Repo.delete_all(from(n in Nonce, where: n.inserted_at < ^nonce_expiry)) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/lti_13/nonces/nonce.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Nonces.Nonce do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "lti_13_nonces" do 6 | field :value, :string 7 | field :domain, :string 8 | belongs_to :lti_user, Lti13.Users.User, foreign_key: :lti_user_id 9 | 10 | timestamps() 11 | end 12 | 13 | @doc false 14 | def changeset(nonce, attrs) do 15 | nonce 16 | |> cast(attrs, [:value, :domain, :lti_user_id]) 17 | |> validate_required([:value]) 18 | |> unique_constraint(:value, name: :value_domain_index) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/lti_13/quiz_score_reporter.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.QuizScoreReporter do 2 | alias Claper.Quizzes 3 | 4 | def report_quiz_score(%Quizzes.Quiz{} = quiz, user_id) do 5 | quiz = 6 | quiz 7 | |> Claper.Repo.preload(lti_resource: [:registration]) 8 | 9 | if quiz.lti_resource do 10 | # Calculate score as percentage of correct answers 11 | score = calculate_score(quiz, user_id) 12 | timestamp = get_timestamp() 13 | 14 | Claper.Workers.QuizLti.post_score(quiz.id, user_id, score, timestamp) |> Oban.insert() 15 | else 16 | # No LTI resource 17 | {:ok, quiz} 18 | end 19 | end 20 | 21 | defp calculate_score(quiz, user_id) do 22 | {correct_answers, total_questions} = Quizzes.calculate_user_score(user_id, quiz.id) 23 | 24 | correct_answers / total_questions * 100 25 | end 26 | 27 | defp get_timestamp do 28 | {:ok, dt} = DateTime.now("Etc/UTC") 29 | DateTime.to_iso8601(dt) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/lti_13/registrations.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Registrations do 2 | import Ecto.Query, warn: false 3 | alias Lti13.Deployments.Deployment 4 | alias Claper.Repo 5 | alias Lti13.Registrations.Registration 6 | 7 | def create_registration(attrs) do 8 | %Registration{} 9 | |> Registration.changeset(attrs) 10 | |> Repo.insert() 11 | end 12 | 13 | def get_registration_deployment(issuer, client_id, deployment_id) do 14 | case Repo.one( 15 | from(d in Deployment, 16 | join: r in Registration, 17 | on: d.registration_id == r.id, 18 | where: 19 | r.issuer == ^issuer and r.client_id == ^client_id and 20 | d.deployment_id == ^deployment_id, 21 | select: {r, d} 22 | ) 23 | ) do 24 | nil -> 25 | nil 26 | 27 | {r, d} -> 28 | {r, d} 29 | end 30 | end 31 | 32 | def get_registration_by_issuer_client_id(issuer, client_id) do 33 | Repo.one( 34 | from(registration in Registration, 35 | where: registration.issuer == ^issuer and registration.client_id == ^client_id, 36 | select: registration 37 | ) 38 | ) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/lti_13/registrations/registration.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Registrations.Registration do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | issuer: String.t() | nil, 8 | user_id: integer() | nil, 9 | client_id: String.t() | nil, 10 | key_set_url: String.t() | nil, 11 | auth_token_url: String.t() | nil, 12 | auth_login_url: String.t() | nil, 13 | auth_server: String.t() | nil, 14 | tool_jwk_id: integer() | nil, 15 | inserted_at: NaiveDateTime.t(), 16 | updated_at: NaiveDateTime.t() 17 | } 18 | 19 | schema "lti_13_registrations" do 20 | field :issuer, :string 21 | field :client_id, :string 22 | field :key_set_url, :string 23 | field :auth_token_url, :string 24 | field :auth_login_url, :string 25 | field :auth_server, :string 26 | 27 | has_many :deployments, Lti13.Deployments.Deployment 28 | belongs_to :tool_jwk, Lti13.Jwks.Jwk, foreign_key: :tool_jwk_id 29 | belongs_to :user, Claper.Accounts.User 30 | 31 | timestamps() 32 | end 33 | 34 | @doc false 35 | def changeset(registration, attrs \\ %{}) do 36 | registration 37 | |> cast(attrs, [ 38 | :issuer, 39 | :user_id, 40 | :client_id, 41 | :key_set_url, 42 | :auth_token_url, 43 | :auth_login_url, 44 | :auth_server, 45 | :tool_jwk_id 46 | ]) 47 | |> validate_required([ 48 | :issuer, 49 | :user_id, 50 | :client_id, 51 | :key_set_url, 52 | :auth_token_url, 53 | :auth_login_url, 54 | :auth_server, 55 | :tool_jwk_id 56 | ]) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/lti_13/resources/resource.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Resources.Resource do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | title: String.t() | nil, 8 | resource_id: integer() | nil, 9 | event_id: integer(), 10 | registration_id: integer(), 11 | line_items_url: String.t() | nil, 12 | inserted_at: NaiveDateTime.t(), 13 | updated_at: NaiveDateTime.t() 14 | } 15 | 16 | schema "lti_13_resources" do 17 | field :title, :string 18 | field :resource_id, :integer 19 | field :line_items_url, :string 20 | 21 | belongs_to :event, Claper.Events.Event 22 | belongs_to :registration, Lti13.Registrations.Registration 23 | 24 | timestamps() 25 | end 26 | 27 | @doc false 28 | def changeset(registration, attrs \\ %{}) do 29 | registration 30 | |> cast(attrs, [:title, :resource_id, :event_id, :line_items_url, :registration_id]) 31 | |> validate_required([:title, :resource_id, :event_id, :registration_id]) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/lti_13/tool/message_validators/message_validator.ex: -------------------------------------------------------------------------------- 1 | defprotocol Lti_1p3.Tool.MessageValidator do 2 | @spec can_validate(any) :: boolean 3 | def can_validate(jwt_body) 4 | 5 | @spec validate(any) :: {:ok} | {:error, String.t()} 6 | def validate(jwt_body) 7 | end 8 | -------------------------------------------------------------------------------- /lib/lti_13/tool/message_validators/resource_message_validator.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Tool.MessageValidators.ResourceMessageValidator do 2 | def can_validate(jwt_body) do 3 | jwt_body["https://purl.imsglobal.org/spec/lti/claim/message_type"] == "LtiResourceLinkRequest" 4 | end 5 | 6 | def validate(jwt_body) do 7 | with {:ok} <- user_sub(jwt_body), 8 | {:ok} <- lti_version(jwt_body), 9 | {:ok} <- roles_claim(jwt_body), 10 | {:ok} <- resource_link_id(jwt_body) do 11 | {:ok} 12 | else 13 | {:error, error} -> {:error, error} 14 | end 15 | end 16 | 17 | defp user_sub(jwt_body) do 18 | case jwt_body["sub"] do 19 | nil -> 20 | {:error, "Must have a user (sub)"} 21 | 22 | _ -> 23 | {:ok} 24 | end 25 | end 26 | 27 | defp lti_version(jwt_body) do 28 | if jwt_body["https://purl.imsglobal.org/spec/lti/claim/version"] != "1.3.0" do 29 | {:error, "Incorrect version, expected 1.3.0"} 30 | else 31 | {:ok} 32 | end 33 | end 34 | 35 | defp roles_claim(jwt_body) do 36 | case jwt_body["https://purl.imsglobal.org/spec/lti/claim/roles"] do 37 | nil -> 38 | {:error, "Missing Roles Claim"} 39 | 40 | _ -> 41 | {:ok} 42 | end 43 | end 44 | 45 | defp resource_link_id(jwt_body) do 46 | case jwt_body["https://purl.imsglobal.org/spec/lti/claim/resource_link"]["id"] do 47 | nil -> 48 | {:error, "Missing Resource Link Id"} 49 | 50 | _ -> 51 | {:ok} 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/lti_13/tool/services/ags/line_item.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Tool.Services.AGS.LineItem do 2 | @derive {Jason.Encoder, except: [:id]} 3 | @enforce_keys [:scoreMaximum, :label, :resourceId] 4 | defstruct [:id, :scoreMaximum, :label, :resourceId] 5 | 6 | # The javascript naming convention here is important to match what the 7 | # LTI AGS standard expects 8 | 9 | @type t() :: %__MODULE__{ 10 | id: String.t(), 11 | scoreMaximum: float, 12 | label: String.t(), 13 | resourceId: String.t() 14 | } 15 | 16 | def parse_resource_id(%__MODULE__{} = line_item) do 17 | case line_item.resourceId do 18 | resource_id -> resource_id 19 | end 20 | end 21 | 22 | def to_resource_id(resource_id) do 23 | Integer.to_string(resource_id) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/lti_13/tool/services/ags/score.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Tool.Services.AGS.Score do 2 | @derive Jason.Encoder 3 | @enforce_keys [ 4 | :timestamp, 5 | :scoreGiven, 6 | :scoreMaximum, 7 | :comment, 8 | :activityProgress, 9 | :gradingProgress, 10 | :userId 11 | ] 12 | defstruct [ 13 | :timestamp, 14 | :scoreGiven, 15 | :scoreMaximum, 16 | :comment, 17 | :activityProgress, 18 | :gradingProgress, 19 | :userId 20 | ] 21 | 22 | # The javascript naming convention here is important to match what the 23 | # LTI AGS standard expects 24 | 25 | @type t() :: %__MODULE__{ 26 | timestamp: String.t(), 27 | scoreGiven: float, 28 | scoreMaximum: float, 29 | comment: String.t(), 30 | activityProgress: String.t(), 31 | gradingProgress: String.t(), 32 | userId: String.t() 33 | } 34 | end 35 | -------------------------------------------------------------------------------- /lib/lti_13/tool/services/nrps/membership.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Tool.Services.NRPS.Membership do 2 | # See https://www.imsglobal.org/spec/lti-nrps/v2p0#context-membership 3 | 4 | @derive Jason.Encoder 5 | @enforce_keys [ 6 | :status, 7 | :name, 8 | :picture, 9 | :given_name, 10 | :middle_name, 11 | :family_name, 12 | :email, 13 | :user_id, 14 | :roles 15 | ] 16 | defstruct [ 17 | :status, 18 | :name, 19 | :picture, 20 | :given_name, 21 | :middle_name, 22 | :family_name, 23 | :email, 24 | :user_id, 25 | :roles 26 | ] 27 | 28 | @type t() :: %__MODULE__{ 29 | status: String.t(), 30 | name: String.t(), 31 | picture: String.t(), 32 | given_name: String.t(), 33 | middle_name: String.t(), 34 | family_name: String.t(), 35 | email: String.t(), 36 | user_id: String.t(), 37 | roles: [String.t()] 38 | } 39 | end 40 | -------------------------------------------------------------------------------- /lib/lti_13/users.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Users do 2 | import Ecto.Query, warn: false 3 | alias Claper.Repo 4 | 5 | alias Lti13.Users.User 6 | 7 | def create_user(attrs) do 8 | %User{} 9 | |> User.changeset(attrs) 10 | |> Repo.insert() 11 | end 12 | 13 | def get_user_by_sub_and_registration_id(sub, registration_id) do 14 | Repo.get_by(User, sub: sub, registration_id: registration_id) 15 | end 16 | 17 | def get_all_users_by_email(email) do 18 | Repo.all(from u in User, where: u.email == ^email) 19 | end 20 | 21 | def remove_user(claper_user, registration_id) do 22 | Repo.delete_all( 23 | from u in User, 24 | where: u.registration_id == ^registration_id and u.user_id == ^claper_user.id 25 | ) 26 | end 27 | 28 | def get_or_create_user( 29 | %{ 30 | sub: sub, 31 | email: email, 32 | registration_id: registration_id 33 | } = attrs 34 | ) do 35 | case get_user_by_sub_and_registration_id(sub, registration_id) do 36 | nil -> create_new_user(attrs, email, registration_id) 37 | %User{} = user -> {:ok, user |> Repo.preload(:user)} 38 | end 39 | end 40 | 41 | defp create_new_user(attrs, email, registration_id) do 42 | with {:ok, claper_user} <- Claper.Accounts.get_user_by_email_or_create(email), 43 | updated_attrs <- 44 | Map.merge(attrs, %{user_id: claper_user.id, registration_id: registration_id}), 45 | {:ok, user} <- create_user(updated_attrs) do 46 | {:ok, user |> Repo.preload(:user)} 47 | else 48 | _ -> {:error, %{reason: :invalid_user, msg: "Invalid user"}} 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/lti_13/users/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Users.User do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | @type t :: %__MODULE__{ 6 | id: integer(), 7 | sub: String.t(), 8 | name: String.t() | nil, 9 | email: String.t(), 10 | roles: [String.t()], 11 | user_id: integer(), 12 | registration_id: integer(), 13 | inserted_at: NaiveDateTime.t(), 14 | updated_at: NaiveDateTime.t() 15 | } 16 | 17 | schema "lti_13_users" do 18 | field :sub, :string 19 | field :name, :string 20 | field :email, :string 21 | field :roles, {:array, :string} 22 | 23 | belongs_to :user, Claper.Accounts.User 24 | belongs_to :registration, Lti13.Registrations.Registration 25 | 26 | timestamps() 27 | end 28 | 29 | @doc false 30 | def changeset(user, attrs) do 31 | user 32 | |> cast(attrs, [ 33 | :sub, 34 | :name, 35 | :roles, 36 | :email, 37 | :user_id, 38 | :registration_id 39 | ]) 40 | |> validate_required([:sub, :name, :email, :roles, :user_id, :registration_id]) 41 | |> unique_constraint(:sub) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/utils/file_upload.ex: -------------------------------------------------------------------------------- 1 | defmodule Utils.FileUpload do 2 | import Mogrify 3 | 4 | def upload(type, path, old_path) when is_atom(type) do 5 | remove_old_file(old_path) 6 | 7 | dest = 8 | Path.join([ 9 | :code.priv_dir(:claper), 10 | "static", 11 | "uploads", 12 | Atom.to_string(type), 13 | Path.basename(path) 14 | ]) 15 | 16 | open(path) |> resize_to_fill("100x100") |> save(in_place: true) 17 | File.cp!(path, dest) 18 | "/uploads/#{Atom.to_string(type)}/#{Path.basename(dest)}" 19 | end 20 | 21 | defp remove_old_file(old_path) do 22 | if old_path do 23 | old_file = Path.join([:code.priv_dir(:claper), "static", old_path]) 24 | if File.exists?(old_file), do: File.rm(old_file) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /phoenix_static_buildpack.config: -------------------------------------------------------------------------------- 1 | node_version=14.19.0 2 | -------------------------------------------------------------------------------- /priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211007130631_create_users_auth_tables.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreateUsersAuthTables do 2 | use Ecto.Migration 3 | 4 | def change do 5 | execute "CREATE EXTENSION IF NOT EXISTS citext", "" 6 | execute "CREATE EXTENSION IF NOT EXISTS pgcrypto", "" 7 | 8 | create table(:users) do 9 | add :uuid, :binary_id, null: false, default: fragment("gen_random_uuid()") 10 | add :email, :citext, null: false 11 | add :is_admin, :boolean, null: false, default: false, null: false 12 | add :confirmed_at, :naive_datetime 13 | timestamps() 14 | end 15 | 16 | create unique_index(:users, [:email]) 17 | create index(:users, [:uuid]) 18 | 19 | create table(:users_tokens) do 20 | add :uuid, :binary_id, null: false, default: fragment("gen_random_uuid()") 21 | add :user_id, references(:users, on_delete: :delete_all) 22 | add :token, :binary, null: false 23 | add :context, :string, null: false 24 | add :sent_to, :string 25 | timestamps(updated_at: false) 26 | end 27 | 28 | create index(:users_tokens, [:user_id]) 29 | create index(:users_tokens, [:uuid]) 30 | create unique_index(:users_tokens, [:context, :token]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211007145520_create_events.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreateEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:events) do 6 | add :uuid, :binary_id, null: false, default: fragment("gen_random_uuid()") 7 | add :name, :string 8 | add :code, :string, null: false 9 | add :user_id, references(:users, on_delete: :delete_all) 10 | add :started_at, :naive_datetime, null: false 11 | add :expired_at, :naive_datetime 12 | timestamps() 13 | end 14 | 15 | create unique_index(:events, [:id]) 16 | create unique_index(:events, [:uuid]) 17 | create index(:events, [:user_id]) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /priv/repo/migrations/20211007152922_create_posts.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreatePosts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:posts) do 6 | add :uuid, :binary_id, null: false, default: fragment("gen_random_uuid()") 7 | add :body, :string, null: false 8 | add :like_count, :integer, default: 0 9 | add :love_count, :integer, default: 0 10 | add :lol_count, :integer, default: 0 11 | add :position, :integer, default: 0 12 | add :name, :string 13 | add :attendee_identifier, :string 14 | add :event_id, references(:events, on_delete: :delete_all) 15 | add :user_id, references(:users, on_delete: :delete_all) 16 | 17 | timestamps() 18 | end 19 | 20 | create unique_index(:posts, [:uuid]) 21 | create unique_index(:posts, [:id]) 22 | create index(:posts, [:attendee_identifier]) 23 | create index(:posts, [:user_id]) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220111171051_create_reactions.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreateReactions do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:reactions) do 6 | add :icon, :string 7 | add :attendee_identifier, :string 8 | add :post_id, references(:posts, on_delete: :delete_all) 9 | add :user_id, references(:users, on_delete: :delete_all) 10 | 11 | timestamps() 12 | end 13 | 14 | create unique_index(:reactions, [:icon, :post_id, :user_id]) 15 | create unique_index(:reactions, [:icon, :post_id, :attendee_identifier]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220226210445_create_presentation_files.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreatePresentationFiles do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:presentation_files) do 6 | add :hash, :string 7 | add :length, :integer 8 | add :status, :string, default: "processing" 9 | add :event_id, references(:events, on_delete: :delete_all) 10 | 11 | timestamps() 12 | end 13 | 14 | create unique_index(:presentation_files, [:hash]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220305222231_create_polls.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreatePolls do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:polls) do 6 | add :title, :string, null: false 7 | add :position, :integer, default: 0 8 | add :presentation_file_id, references(:presentation_files, on_delete: :nilify_all) 9 | add :enabled, :boolean, default: true 10 | 11 | timestamps() 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220305223506_create_poll_opts.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreatePollOpts do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:poll_opts) do 6 | add :content, :string, null: false 7 | add :vote_count, :integer, default: 0 8 | add :poll_id, references(:polls, on_delete: :delete_all) 9 | 10 | timestamps() 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220314171347_create_activity_leaders.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreateActivityLeaders do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:activity_leaders) do 6 | add :event_id, references(:events, on_delete: :delete_all) 7 | add :email, :string, null: false 8 | 9 | timestamps() 10 | end 11 | 12 | create unique_index(:activity_leaders, [:event_id, :email]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220409094249_create_presentation_states.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreatePresentationStates do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:presentation_states) do 6 | add :presentation_file_id, references(:presentation_files, on_delete: :delete_all) 7 | add :position, :integer, default: 0 8 | add :chat_visible, :boolean, default: false 9 | add :poll_visible, :boolean, default: false 10 | add :join_screen_visible, :boolean, default: false 11 | add :banned, {:array, :string}, default: [] 12 | 13 | timestamps() 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220418194055_create_poll_votes.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreatePollVotes do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:poll_votes) do 6 | add :attendee_identifier, :string 7 | add :poll_id, references(:polls, on_delete: :delete_all) 8 | add :poll_opt_id, references(:poll_opts, on_delete: :delete_all) 9 | add :user_id, references(:users, on_delete: :delete_all) 10 | 11 | timestamps() 12 | end 13 | 14 | create unique_index(:poll_votes, [:poll_id, :user_id]) 15 | create unique_index(:poll_votes, [:poll_id, :attendee_identifier]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220419141142_create_stats.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreateStats do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:stats) do 6 | add :event_id, references(:events, on_delete: :delete_all) 7 | add :status, :string 8 | 9 | timestamps() 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220420124141_events_add_audience_peak_column.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.PresentationFilesAddAudiencePeakColumn do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:events) do 6 | add :audience_peak, :integer, default: 1 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20220822205711_add_hashed_password_to_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddHashedPasswordToUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :hashed_password, :string, null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230218180723_create_forms.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreateForms do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:forms) do 6 | add :title, :string, null: false 7 | add :position, :integer, default: 0 8 | add :enabled, :boolean, default: true 9 | add :presentation_file_id, references(:presentation_files, on_delete: :nilify_all) 10 | add :fields, :map, default: "[]" 11 | 12 | timestamps() 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230218181013_create_form_submits.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreateFormSubmits do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:form_submits) do 6 | add :attendee_identifier, :string 7 | add :form_id, references(:forms, on_delete: :delete_all) 8 | add :user_id, references(:users, on_delete: :delete_all) 9 | add :response, :map, default: "[]" 10 | 11 | timestamps() 12 | end 13 | 14 | create index(:form_submits, [:form_id, :user_id]) 15 | create index(:form_submits, [:form_id, :attendee_identifier]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230419205637_add_multiple_from_polls.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddMultipleFromPolls do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:polls) do 6 | add :multiple, :boolean, default: false 7 | end 8 | 9 | drop index(:poll_votes, [:poll_id, :user_id]), mode: :cascade 10 | drop index(:poll_votes, [:poll_id, :attendee_identifier]), mode: :cascade 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230421093834_add_chat_enabled_to_presentation_states.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddChatEnabledToPresentationStates do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:presentation_states) do 6 | add :chat_enabled, :boolean, default: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20230809172244_add_anonymous_chat_enabled_to_presentation_states.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddAnonymousChatEnabledToPresentationStates do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:presentation_states) do 6 | add :anonymous_chat_enabled, :boolean, default: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20231020175202_add_pinned_to_posts_and_presentation_states.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddPinnedToPostsAndPresentationStates do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:presentation_states) do 6 | add :show_only_pinned, :boolean, default: false 7 | end 8 | 9 | alter table(:posts) do 10 | add :pinned, :boolean, default: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20231028144823_create_embeds.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreateEmbeds do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:embeds) do 6 | add :title, :string, null: false 7 | add :content, :text, null: false 8 | add :position, :integer, default: 0 9 | add :enabled, :boolean, default: true 10 | add :attendee_visibility, :boolean, default: false 11 | add :presentation_file_id, references(:presentation_files, on_delete: :nilify_all) 12 | 13 | timestamps() 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240323140827_add_message_reaction_enabled_to_presentation_states.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddMessageReactionEnabledToPresentationStates do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:presentation_states) do 6 | add :message_reaction_enabled, :boolean, default: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240405111550_add_show_poll_results_enabled_to_presentation_states.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddShowPollResultsEnabledToPresentationStates do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:presentation_states) do 6 | add :show_poll_results_enabled, :boolean, default: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240407090614_add_locale_to_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddTimezoneAndLocaleToUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :locale, :string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240729152555_create_oidc_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddOidcUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:oidc_users) do 6 | add :sub, :string, null: false 7 | add :name, :string 8 | add :email, :string 9 | add :issuer, :string, null: false 10 | add :provider, :string, null: false 11 | add :id_token, :text 12 | add :refresh_token, :text 13 | add :access_token, :text 14 | add :expires_at, :naive_datetime 15 | add :photo_url, :string 16 | add :groups, {:array, :string} 17 | add :roles, :string 18 | add :organization, :string 19 | add :user_id, references(:users, on_delete: :delete_all) 20 | 21 | timestamps() 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240730123205_remove_is_admin_from_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.RemoveIsAdminFromUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | remove :is_admin 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240730123331_add_is_randomized_password_to_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddIsRandomizedPasswordToUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :is_randomized_password, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240731132357_add_provider_to_embeds.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddProviderToEmbeds do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:embeds) do 6 | add :provider, :string, default: "custom" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240801084601_add_show_results_to_polls.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddShowResultsToPolls do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:polls) do 6 | add :show_results, :boolean, default: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240801084731_remove_show_poll_results_enabled_from_presentation_states.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.RemoveShowPollResultsEnabledFromPresentationStates do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:presentation_states) do 6 | remove :show_poll_results_enabled 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240801130739_remove_uniqueness_of_hash_from_presentation_files.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.RemoveUniquenessOfHashFromPresentationFiles do 2 | use Ecto.Migration 3 | 4 | def up do 5 | drop_if_exists index(:presentation_files, [:hash]) 6 | create unique_index(:presentation_files, [:hash, :event_id]) 7 | end 8 | 9 | def down do 10 | drop_if_exists index(:presentation_files, [:hash, :event_id]) 11 | create unique_index(:presentation_files, [:hash]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20240928085505_create_quizzes.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.CreateQuizzes do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:quizzes) do 6 | add :title, :string, size: 255 7 | add :position, :integer, default: 0 8 | add :presentation_file_id, references(:presentation_files, on_delete: :delete_all) 9 | add :enabled, :boolean, default: false 10 | add :show_results, :boolean, default: false 11 | 12 | timestamps() 13 | end 14 | 15 | create table(:quiz_questions) do 16 | add :content, :string, size: 255 17 | add :type, :string, default: "qcm" 18 | add :quiz_id, references(:quizzes, on_delete: :delete_all) 19 | 20 | timestamps() 21 | end 22 | 23 | create table(:quiz_question_opts) do 24 | add :content, :string, size: 255 25 | add :is_correct, :boolean, default: false 26 | add :response_count, :integer, default: 0 27 | add :quiz_question_id, references(:quiz_questions, on_delete: :delete_all) 28 | 29 | timestamps() 30 | end 31 | 32 | create table(:quiz_responses) do 33 | add :attendee_identifier, :string 34 | add :quiz_id, references(:quizzes, on_delete: :delete_all) 35 | add :quiz_question_id, references(:quiz_questions, on_delete: :delete_all) 36 | add :quiz_question_opt_id, references(:quiz_question_opts, on_delete: :delete_all) 37 | add :user_id, references(:users, on_delete: :delete_all) 38 | 39 | timestamps() 40 | end 41 | 42 | create index(:quizzes, [:presentation_file_id]) 43 | create index(:quiz_questions, [:quiz_id]) 44 | create index(:quiz_question_opts, [:quiz_question_id]) 45 | create index(:quiz_responses, [:quiz_id]) 46 | create index(:quiz_responses, [:quiz_question_id]) 47 | create index(:quiz_responses, [:quiz_question_opt_id]) 48 | create index(:quiz_responses, [:user_id]) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /priv/repo/migrations/20241128102850_add_attendees_columns_to_stats.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddAttendeesColumnsToStats do 2 | use Ecto.Migration 3 | 4 | def up do 5 | alter table(:stats) do 6 | add :attendee_identifier, :string 7 | add :user_id, references(:users, on_delete: :delete_all) 8 | remove :status 9 | end 10 | 11 | create unique_index(:stats, [:event_id, :user_id]) 12 | create unique_index(:stats, [:event_id, :attendee_identifier]) 13 | end 14 | 15 | def down do 16 | drop unique_index(:stats, [:event_id, :attendee_identifier]) 17 | drop unique_index(:stats, [:event_id, :user_id]) 18 | 19 | alter table(:stats) do 20 | remove :attendee_identifier 21 | remove :user_id 22 | add :status, :string 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /priv/repo/migrations/20241207115352_add_lti_line_item_columns_to_quizzes_and_lti_resources.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddLtiLineItemColumnsToQuizzesAndEvents do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:quizzes) do 6 | add :lti_line_item_url, :string 7 | end 8 | 9 | alter table(:lti_13_resources) do 10 | add :line_items_url, :string 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20241207162344_add_user_id_to_lti_registrations.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddUserIdToLtiRegistrations do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:lti_13_registrations) do 6 | add :user_id, references(:users, on_delete: :delete_all) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20241207195849_add_oban_jobs_table.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddObanJobsTable do 2 | use Ecto.Migration 3 | 4 | def up do 5 | Oban.Migration.up(version: 12) 6 | end 7 | 8 | # We specify `version: 1` in `down`, ensuring that we'll roll all the way back down if 9 | # necessary, regardless of which version we've migrated `up` to. 10 | def down do 11 | Oban.Migration.down(version: 1) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /priv/repo/migrations/20241223150438_add_lti_resource_id_to_quizzes.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddLtiResourceIdToQuizzes do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:quizzes) do 6 | add :lti_resource_id, references(:lti_13_resources, on_delete: :delete_all) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20241228162732_add_deleted_at_to_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddDeletedAtToUsers do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:users) do 6 | add :deleted_at, :naive_datetime, null: true 7 | end 8 | 9 | create index(:users, [:deleted_at]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/repo/migrations/20250102174720_add_allow_anonymous_to_quizzes.exs: -------------------------------------------------------------------------------- 1 | defmodule Claper.Repo.Migrations.AddAllowAnonymousToQuizzes do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:quizzes) do 6 | add :allow_anonymous, :boolean, default: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Claper.Repo.insert!(%Claper.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | 13 | # create a default active lti_1p3 jwk 14 | if !Claper.Repo.get_by(Lti13.Jwks.Jwk, id: 1) do 15 | %{private_key: private_key} = Lti13.Jwks.Utils.KeyGenerator.generate_key_pair() 16 | 17 | Lti13.Jwks.create_jwk(%{ 18 | pem: private_key, 19 | typ: "JWT", 20 | alg: "RS256", 21 | kid: UUID.uuid4(), 22 | active: true 23 | }) 24 | end 25 | -------------------------------------------------------------------------------- /priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/favicon.ico -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-100.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-100.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-100.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-100.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-100.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-100.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-100.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-100.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-100italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-100italic.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-100italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-100italic.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-100italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-100italic.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-100italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-100italic.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-300.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-300.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-300.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-300.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-300italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-300italic.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-300italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-300italic.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-300italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-300italic.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-300italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-300italic.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-500.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-500.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-500.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-500.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-500italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-500italic.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-500italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-500italic.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-500italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-500italic.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-500italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-500italic.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-700.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-700.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-700.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-700.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-700italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-700italic.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-700italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-700italic.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-700italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-700italic.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-700italic.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-900.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-900.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-900.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-900.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-900italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-900italic.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-900italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-900italic.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-900italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-900italic.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-900italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-900italic.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-italic.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-italic.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-italic.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-italic.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-regular.eot -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-regular.ttf -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-regular.woff -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/roboto-v29-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/fonts/Roboto/roboto-v29-latin-regular.woff2 -------------------------------------------------------------------------------- /priv/static/fonts/Roboto/type.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /priv/static/images/base-slide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/base-slide.jpg -------------------------------------------------------------------------------- /priv/static/images/client-login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/client-login.jpg -------------------------------------------------------------------------------- /priv/static/images/education.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/education.jpg -------------------------------------------------------------------------------- /priv/static/images/emails/bg-white-rombo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/emails/bg-white-rombo.png -------------------------------------------------------------------------------- /priv/static/images/emails/change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/emails/change.png -------------------------------------------------------------------------------- /priv/static/images/emails/lock4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/emails/lock4.png -------------------------------------------------------------------------------- /priv/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/favicon.png -------------------------------------------------------------------------------- /priv/static/images/icons/calendar-clear-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | Calendar Clear 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/static/images/icons/chatbubble-ellipses-outline.svg: -------------------------------------------------------------------------------- 1 | Chatbubble Ellipses -------------------------------------------------------------------------------- /priv/static/images/icons/create-outline.svg: -------------------------------------------------------------------------------- 1 | Create -------------------------------------------------------------------------------- /priv/static/images/icons/danger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/icons/danger.png -------------------------------------------------------------------------------- /priv/static/images/icons/easel-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | Easel 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/static/images/icons/easel.svg: -------------------------------------------------------------------------------- 1 | 2 | Easel 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/static/images/icons/ellipsis-horizontal-white.svg: -------------------------------------------------------------------------------- 1 | Ellipsis Horizontal -------------------------------------------------------------------------------- /priv/static/images/icons/ellipsis-horizontal.svg: -------------------------------------------------------------------------------- 1 | Ellipsis Horizontal -------------------------------------------------------------------------------- /priv/static/images/icons/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/icons/email.png -------------------------------------------------------------------------------- /priv/static/images/icons/exit-outline.svg: -------------------------------------------------------------------------------- 1 | Exit -------------------------------------------------------------------------------- /priv/static/images/icons/eye-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | Eye 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/static/images/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | Eye 3 | 4 | 5 | -------------------------------------------------------------------------------- /priv/static/images/icons/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/images/icons/lms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/icons/lms.png -------------------------------------------------------------------------------- /priv/static/images/icons/menu-outline.svg: -------------------------------------------------------------------------------- 1 | Menu -------------------------------------------------------------------------------- /priv/static/images/icons/online-users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /priv/static/images/icons/openid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/icons/openid.png -------------------------------------------------------------------------------- /priv/static/images/icons/reader-outline.svg: -------------------------------------------------------------------------------- 1 | Reader -------------------------------------------------------------------------------- /priv/static/images/icons/reload-outline.svg: -------------------------------------------------------------------------------- 1 | Reload -------------------------------------------------------------------------------- /priv/static/images/icons/send.svg: -------------------------------------------------------------------------------- 1 | Send -------------------------------------------------------------------------------- /priv/static/images/icons/star.svg: -------------------------------------------------------------------------------- 1 | Star -------------------------------------------------------------------------------- /priv/static/images/icons/time-green.svg: -------------------------------------------------------------------------------- 1 | Time -------------------------------------------------------------------------------- /priv/static/images/icons/time.svg: -------------------------------------------------------------------------------- 1 | 2 | Time 3 | 4 | -------------------------------------------------------------------------------- /priv/static/images/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /priv/static/images/interaction-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/interaction-icons.png -------------------------------------------------------------------------------- /priv/static/images/lms-platforms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/lms-platforms.png -------------------------------------------------------------------------------- /priv/static/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/loading.gif -------------------------------------------------------------------------------- /priv/static/images/logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/logo-large.png -------------------------------------------------------------------------------- /priv/static/images/logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /priv/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/logo.png -------------------------------------------------------------------------------- /priv/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /priv/static/images/mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/mobile.png -------------------------------------------------------------------------------- /priv/static/images/new-event-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/new-event-bg.png -------------------------------------------------------------------------------- /priv/static/images/partners/lmddc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/partners/lmddc.png -------------------------------------------------------------------------------- /priv/static/images/partners/pixilearn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/partners/pixilearn.png -------------------------------------------------------------------------------- /priv/static/images/partners/uccs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/partners/uccs.png -------------------------------------------------------------------------------- /priv/static/images/plans/free-plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/plans/free-plan.png -------------------------------------------------------------------------------- /priv/static/images/plans/gold-plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/plans/gold-plan.png -------------------------------------------------------------------------------- /priv/static/images/plans/platinum-plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/plans/platinum-plan.png -------------------------------------------------------------------------------- /priv/static/images/plans/silver-plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/plans/silver-plan.png -------------------------------------------------------------------------------- /priv/static/images/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ClaperCo/Claper/c745f374894165196c114d80a4eb2dcb1c10583b/priv/static/images/preview.png -------------------------------------------------------------------------------- /priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /rel/env.bat.eex: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem Set the release to work across nodes. 3 | rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". 4 | rem set RELEASE_DISTRIBUTION=name 5 | rem set RELEASE_NODE=<%= @release.name %> 6 | -------------------------------------------------------------------------------- /rel/env.sh.eex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Sets and enables heart (recommended only in daemon mode) 4 | # case $RELEASE_COMMAND in 5 | # daemon*) 6 | # HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" 7 | # export HEART_COMMAND 8 | # export ELIXIR_ERL_OPTIONS="-heart" 9 | # ;; 10 | # *) 11 | # ;; 12 | # esac 13 | 14 | # Set the release to work across nodes. 15 | # RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". 16 | export RELEASE_DISTRIBUTION=sname 17 | export RELEASE_NODE=<%= @release.name %> 18 | 19 | #export POD_A_RECORD=$(echo $POD_IP | sed 's/\./-/g') 20 | #export RELEASE_DISTRIBUTION=name 21 | #export RELEASE_NODE=<%= @release.name %>@$(echo $POD_A_RECORD).$(echo $NAMESPACE).pod.cluster.local 22 | -------------------------------------------------------------------------------- /rel/remote.vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Number of dirty schedulers doing IO work (file, sockets, and others) 5 | ##+SDio 5 6 | 7 | ## Increase number of concurrent ports/sockets 8 | ##+Q 65536 9 | 10 | ## Tweak GC to run more often 11 | ##-env ERL_FULLSWEEP_AFTER 10 12 | -------------------------------------------------------------------------------- /rel/vm.args.eex: -------------------------------------------------------------------------------- 1 | ## Customize flags given to the VM: https://erlang.org/doc/man/erl.html 2 | ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here 3 | 4 | ## Number of dirty schedulers doing IO work (file, sockets, and others) 5 | ##+SDio 5 6 | 7 | ## Increase number of concurrent ports/sockets 8 | ##+Q 65536 9 | 10 | ## Tweak GC to run more often 11 | ##-env ERL_FULLSWEEP_AFTER 10 12 | -------------------------------------------------------------------------------- /reset-db.sh: -------------------------------------------------------------------------------- 1 | docker stop claper-db 2 | docker rm claper-db 3 | docker run -p 5432:5432 -e POSTGRES_PASSWORD=claper -e POSTGRES_USER=claper -e POSTGRES_DB=claper --name claper-db -d postgres:15 4 | sleep 5 5 | mix ecto.migrate 6 | mix run priv/repo/seeds.exs -------------------------------------------------------------------------------- /test/claper_web/controllers/lti_controller.exs: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.LtiControllerTest do 2 | use ClaperWeb.ConnCase, async: true 3 | 4 | describe "GET /.well-known/jwks.json" do 5 | test "returns the public key", %{conn: conn} do 6 | conn = get(conn, ~p"/.well-known/jwks.json") 7 | response = json_response(conn, 200) 8 | assert response["keys"] 9 | end 10 | end 11 | 12 | describe "GET /lti/login" do 13 | test "renders log in page", %{conn: conn} do 14 | conn = get(conn, ~p"/users/log_in") 15 | response = html_response(conn, 200) 16 | assert response =~ "Email address" 17 | end 18 | 19 | test "redirects if already logged in", %{conn: conn, user: user} do 20 | conn = conn |> log_in_user(user) |> get(~p"/users/log_in") 21 | assert redirected_to(conn) == "/events" 22 | end 23 | end 24 | 25 | describe "POST /lti/login" do 26 | test "logs the user out", %{conn: conn, user: user} do 27 | conn = conn |> log_in_user(user) |> delete(~p"/users/log_out") 28 | assert redirected_to(conn) == "/" 29 | refute get_session(conn, :user_token) 30 | end 31 | 32 | test "succeeds even if the user is not logged in", %{conn: conn} do 33 | conn = delete(conn, ~p"/users/log_out") 34 | assert redirected_to(conn) == "/" 35 | refute get_session(conn, :user_token) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/claper_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.PageControllerTest do 2 | use ClaperWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, "/") 6 | assert html_response(conn, 200) =~ "About" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/claper_web/controllers/user_session_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.UserSessionControllerTest do 2 | use ClaperWeb.ConnCase, async: true 3 | 4 | import Claper.AccountsFixtures 5 | 6 | setup do 7 | %{user: user_fixture()} 8 | end 9 | 10 | describe "GET /users/log_in" do 11 | test "renders log in page", %{conn: conn} do 12 | conn = get(conn, ~p"/users/log_in") 13 | response = html_response(conn, 200) 14 | assert response =~ "Email address" 15 | end 16 | 17 | test "redirects if already logged in", %{conn: conn, user: user} do 18 | conn = conn |> log_in_user(user) |> get(~p"/users/log_in") 19 | assert redirected_to(conn) == "/events" 20 | end 21 | end 22 | 23 | describe "DELETE /users/log_out" do 24 | test "logs the user out", %{conn: conn, user: user} do 25 | conn = conn |> log_in_user(user) |> delete(~p"/users/log_out") 26 | assert redirected_to(conn) == "/" 27 | refute get_session(conn, :user_token) 28 | end 29 | 30 | test "succeeds even if the user is not logged in", %{conn: conn} do 31 | conn = delete(conn, ~p"/users/log_out") 32 | assert redirected_to(conn) == "/" 33 | refute get_session(conn, :user_token) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/claper_web/live/post_live_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.PostLiveTest do 2 | use ClaperWeb.ConnCase 3 | 4 | import Phoenix.LiveViewTest 5 | import Claper.{PresentationsFixtures, PostsFixtures} 6 | 7 | defp create_event(params) do 8 | presentation_file = presentation_file_fixture(%{user: params.user}, [:event]) 9 | presentation_state_fixture(%{presentation_file: presentation_file}) 10 | post = post_fixture(%{user: params.user, event: presentation_file.event}) 11 | params |> Map.put(:presentation_file, presentation_file) |> Map.put(:post, post) 12 | end 13 | 14 | describe "Index" do 15 | setup [:register_and_log_in_user, :create_event] 16 | 17 | test "list posts", %{conn: conn, presentation_file: presentation_file} do 18 | {:ok, _index_live, html} = 19 | live(conn, ~p"/e/#{presentation_file.event.code}") 20 | 21 | assert html =~ "some body" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/lti_13/jwks/utils/key_generator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Jwks.Utils.KeyGeneratorTest do 2 | use Claper.DataCase 3 | 4 | alias Lti13.Jwks.Utils.KeyGenerator 5 | 6 | describe "key generator" do 7 | test "passphrase/0 generates a random passphrase of size 256" do 8 | assert String.length(KeyGenerator.passphrase()) == 256 9 | end 10 | 11 | test "generate_key_pair/0 generates a public and private key pair" do 12 | keypair = KeyGenerator.generate_key_pair() 13 | 14 | assert Map.has_key?(keypair, :public_key) 15 | assert Map.has_key?(keypair, :private_key) 16 | assert Map.has_key?(keypair, :key_id) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/lti_13/registrations_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Lti13.RegistrationsTest do 2 | use Claper.DataCase 3 | 4 | alias Lti13.Registrations 5 | 6 | import Lti13.JwksFixtures 7 | import Claper.AccountsFixtures 8 | 9 | describe "registrations" do 10 | test "create and get registration by issuer client id" do 11 | jwk = jwk_fixture() 12 | user = confirmed_user_fixture() 13 | 14 | registration = %{ 15 | issuer: "some issuer", 16 | client_id: "some client_id", 17 | key_set_url: "some key_set_url", 18 | auth_token_url: "some auth_token_url", 19 | auth_login_url: "some auth_login_url", 20 | auth_server: "some auth_server", 21 | user_id: user.id, 22 | tool_jwk_id: jwk.id 23 | } 24 | 25 | assert {:ok, 26 | %Registrations.Registration{issuer: "some issuer", client_id: "some client_id"}} = 27 | Registrations.create_registration(registration) 28 | 29 | assert %Registrations.Registration{issuer: "some issuer", client_id: "some client_id"} = 30 | Registrations.get_registration_by_issuer_client_id("some issuer", "some client_id") 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/lti_13/tool/oidc_login_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Lti13.Tool.OidcLoginTest do 2 | end 3 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule ClaperWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use ClaperWeb.ChannelCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with channels 23 | import Phoenix.ChannelTest 24 | import ClaperWeb.ChannelCase 25 | 26 | # The default endpoint for testing 27 | @endpoint ClaperWeb.Endpoint 28 | end 29 | end 30 | 31 | setup tags do 32 | pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Claper.Repo, shared: not tags[:async]) 33 | on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) 34 | :ok 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | we enable the SQL sandbox, so changes done to the database 11 | are reverted at the end of every test. If you are using 12 | PostgreSQL, you can even run database tests asynchronously 13 | by setting `use Claper.DataCase, async: true`, although 14 | this option is not recommended for other databases. 15 | """ 16 | 17 | use ExUnit.CaseTemplate 18 | 19 | using do 20 | quote do 21 | use Oban.Testing, repo: Claper.Repo 22 | alias Claper.Repo 23 | 24 | import Ecto 25 | import Ecto.Changeset 26 | import Ecto.Query 27 | import Claper.DataCase 28 | end 29 | end 30 | 31 | setup tags do 32 | pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Claper.Repo, shared: not tags[:async]) 33 | on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) 34 | :ok 35 | end 36 | 37 | @doc """ 38 | A helper that transforms changeset errors into a map of messages. 39 | 40 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 41 | assert "password is too short" in errors_on(changeset).password 42 | assert %{password: ["password is too short"]} = errors_on(changeset) 43 | 44 | """ 45 | def errors_on(changeset) do 46 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 47 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 48 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 49 | end) 50 | end) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/support/fixtures/accounts_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.AccountsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Claper.Accounts` context. 5 | """ 6 | 7 | def unique_user_email, do: "user#{System.unique_integer()}@example.com" 8 | def valid_user_password, do: "hello world!" 9 | 10 | def valid_user_attributes(attrs \\ %{}) do 11 | Enum.into(attrs, %{ 12 | email: unique_user_email(), 13 | password: valid_user_password(), 14 | confirmed_at: NaiveDateTime.utc_now() 15 | }) 16 | end 17 | 18 | def no_valid_user_attributes(attrs \\ %{}) do 19 | Enum.into(attrs, %{ 20 | email: unique_user_email(), 21 | password: valid_user_password() 22 | }) 23 | end 24 | 25 | def user_fixture(attrs \\ %{}) do 26 | {:ok, user} = 27 | attrs 28 | |> no_valid_user_attributes() 29 | |> Claper.Accounts.register_user() 30 | 31 | user 32 | end 33 | 34 | def confirmed_user_fixture(attrs \\ %{}) do 35 | {:ok, user} = 36 | attrs 37 | |> valid_user_attributes() 38 | |> Claper.Accounts.register_user() 39 | 40 | user 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/support/fixtures/embeds__fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.EmbedsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Claper.Embeds` context. 5 | """ 6 | 7 | require Claper.UtilFixture 8 | 9 | @doc """ 10 | Generate a embed. 11 | """ 12 | def embed_fixture(attrs \\ %{}, preload \\ []) do 13 | {:ok, embed} = 14 | attrs 15 | |> Enum.into(%{ 16 | title: "some title", 17 | content: 18 | "", 19 | position: 0, 20 | provider: "custom", 21 | enabled: true, 22 | attendee_visibility: true 23 | }) 24 | |> Claper.Embeds.create_embed() 25 | 26 | Claper.UtilFixture.merge_preload(embed, preload, %{}) 27 | end 28 | 29 | def embed_youtube_fixture(attrs \\ %{}, preload \\ []) do 30 | {:ok, embed} = 31 | attrs 32 | |> Enum.into(%{ 33 | title: "some title", 34 | content: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", 35 | position: 1, 36 | provider: "youtube", 37 | enabled: true, 38 | attendee_visibility: true 39 | }) 40 | |> Claper.Embeds.create_embed() 41 | 42 | Claper.UtilFixture.merge_preload(embed, preload, %{}) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/support/fixtures/events_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.EventsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Claper.Events` context. 5 | """ 6 | 7 | import Claper.{AccountsFixtures} 8 | 9 | require Claper.UtilFixture 10 | 11 | @doc """ 12 | Generate a event. 13 | """ 14 | def event_fixture(attrs \\ %{}, preload \\ []) do 15 | assoc = %{user: attrs[:user] || user_fixture()} 16 | 17 | {:ok, event} = 18 | attrs 19 | |> Enum.into(%{ 20 | name: "some name", 21 | code: :crypto.strong_rand_bytes(4) |> Base.url_encode64() |> binary_part(0, 8), 22 | uuid: Ecto.UUID.generate(), 23 | user_id: assoc.user.id, 24 | started_at: NaiveDateTime.utc_now(), 25 | expired_at: nil 26 | }) 27 | |> Claper.Events.create_event() 28 | 29 | Claper.UtilFixture.merge_preload(event, preload, assoc) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/support/fixtures/forms_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.FormsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Claper.Forms` context. 5 | """ 6 | 7 | import Claper.{AccountsFixtures, PresentationsFixtures} 8 | 9 | require Claper.UtilFixture 10 | 11 | @doc """ 12 | Generate a form. 13 | """ 14 | def form_fixture(attrs \\ %{}, preload \\ []) do 15 | {:ok, form} = 16 | attrs 17 | |> Enum.into(%{ 18 | title: "some title", 19 | position: 0, 20 | enabled: true, 21 | fields: [%{name: "Name", type: "text"}] 22 | }) 23 | |> Claper.Forms.create_form() 24 | 25 | Claper.UtilFixture.merge_preload(form, preload, %{}) 26 | end 27 | 28 | @doc """ 29 | Generate a form submit. 30 | """ 31 | def form_submit_fixture(attrs \\ %{}) do 32 | presentation_file = presentation_file_fixture() 33 | form = form_fixture(%{presentation_file_id: presentation_file.id}) 34 | assoc = %{form: form} 35 | 36 | {:ok, form_submit} = 37 | attrs 38 | |> Enum.into(%{ 39 | form_id: assoc.form.id, 40 | user_id: user_fixture().id, 41 | response: %{"Test" => "some option 1", "Test2" => "some option 2"} 42 | }) 43 | |> Claper.Forms.create_form_submit() 44 | 45 | form_submit 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/support/fixtures/lti_13/deployments_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.DeploymentsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Lti13.Deployments` context. 5 | """ 6 | 7 | import Lti13.RegistrationsFixtures 8 | 9 | @doc """ 10 | Generate a deployment. 11 | """ 12 | def deployment_fixture(attrs \\ %{}) do 13 | registration = registration_fixture() 14 | 15 | {:ok, deployment} = 16 | attrs 17 | |> Enum.into(%{ 18 | deployment_id: 1, 19 | registration_id: registration.id 20 | }) 21 | |> Lti13.Deployments.create_deployment() 22 | 23 | deployment 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/support/fixtures/lti_13/jwks_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.JwksFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Lti13.Jwks` context. 5 | """ 6 | 7 | @doc """ 8 | Generate a jwk. 9 | """ 10 | def jwk_fixture(attrs \\ %{}) do 11 | {:ok, jwk} = 12 | attrs 13 | |> Enum.into(%{ 14 | pem: "some pem", 15 | typ: "some typ", 16 | alg: "some alg", 17 | kid: "some kid", 18 | use: "some use", 19 | active: true 20 | }) 21 | |> Lti13.Jwks.create_jwk() 22 | 23 | jwk 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/support/fixtures/lti_13/registrations_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.RegistrationsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Lti13.Registrations` context. 5 | """ 6 | 7 | import Lti13.JwksFixtures 8 | import Claper.AccountsFixtures 9 | 10 | @doc """ 11 | Generate a registration. 12 | """ 13 | def registration_fixture(attrs \\ %{}) do 14 | jwk = jwk_fixture() 15 | user = confirmed_user_fixture() 16 | 17 | {:ok, registration} = 18 | attrs 19 | |> Enum.into(%{ 20 | issuer: "https://example.com", 21 | client_id: UUID.uuid4(), 22 | auth_token_url: "https://example.com/auth_token_url", 23 | auth_login_url: "https://example.com/auth_login_url", 24 | key_set_url: "https://example.com/key_set_url", 25 | auth_server: "https://example.com/", 26 | tool_jwk_id: attrs[:tool_jwk_id] || jwk.id, 27 | user_id: user.id 28 | }) 29 | |> Lti13.Registrations.create_registration() 30 | 31 | registration 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/support/fixtures/lti_13/users_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Lti13.UsersFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Lti13.Users` context. 5 | """ 6 | 7 | @doc """ 8 | Generate a user. 9 | """ 10 | def user_fixture(attrs \\ %{}) do 11 | claper_user = Claper.AccountsFixtures.user_fixture() 12 | registration = Lti13.RegistrationsFixtures.registration_fixture() 13 | 14 | {:ok, user} = 15 | attrs 16 | |> Enum.into(%{ 17 | sub: "a6d5c443-1f51-4783-ba1a-7686ffe3b54a", 18 | name: "John Doe", 19 | email: "john#{System.unique_integer([:positive])}@doe.edu", 20 | user_id: claper_user.id, 21 | registration_id: registration.id, 22 | roles: [ 23 | "http://purl.imsglobal.org/vocab/lis/v2/system/person#User", 24 | "http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student" 25 | ] 26 | }) 27 | |> Lti13.Users.create_user() 28 | 29 | user 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/support/fixtures/polls_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.PollsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Claper.Polls` context. 5 | """ 6 | 7 | import Claper.{AccountsFixtures, PresentationsFixtures} 8 | 9 | require Claper.UtilFixture 10 | 11 | @doc """ 12 | Generate a poll. 13 | """ 14 | def poll_fixture(attrs \\ %{}, preload \\ []) do 15 | {:ok, poll} = 16 | attrs 17 | |> Enum.into(%{ 18 | title: "some title", 19 | position: 0, 20 | multiple: false, 21 | enabled: true, 22 | poll_opts: [ 23 | %{content: "some option 1", vote_count: 0}, 24 | %{content: "some option 2", vote_count: 0} 25 | ] 26 | }) 27 | |> Claper.Polls.create_poll() 28 | 29 | Claper.UtilFixture.merge_preload(poll, preload, %{}) 30 | end 31 | 32 | @doc """ 33 | Generate a poll_vote. 34 | """ 35 | def poll_vote_fixture(attrs \\ %{}) do 36 | presentation_file = presentation_file_fixture() 37 | poll = poll_fixture(%{presentation_file_id: presentation_file.id}) 38 | [poll_opt | _] = poll.poll_opts 39 | assoc = %{poll: poll} 40 | 41 | {:ok, poll_vote} = 42 | attrs 43 | |> Enum.into(%{ 44 | poll_id: assoc.poll.id, 45 | poll_opt_id: poll_opt.id, 46 | user_id: user_fixture().id 47 | }) 48 | |> Claper.Polls.create_poll_vote() 49 | 50 | poll_vote 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/support/fixtures/posts_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.PostsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Claper.Posts` context. 5 | """ 6 | 7 | import Claper.{AccountsFixtures, EventsFixtures} 8 | 9 | require Claper.UtilFixture 10 | 11 | @doc """ 12 | Generate a post. 13 | """ 14 | def post_fixture(attrs \\ %{}, preload \\ []) do 15 | user = attrs[:user] || user_fixture() 16 | event = attrs[:event] || event_fixture() 17 | assoc = %{user: user, event: event} 18 | 19 | {:ok, post} = 20 | Claper.Posts.create_post( 21 | assoc.event, 22 | attrs 23 | |> Enum.into(%{ 24 | body: "some body", 25 | like_count: 42, 26 | position: 0, 27 | uuid: Ecto.UUID.generate(), 28 | user_id: assoc.user.id 29 | }) 30 | ) 31 | 32 | Claper.UtilFixture.merge_preload(post, preload, assoc) 33 | end 34 | 35 | @doc """ 36 | Generate a reaction. 37 | """ 38 | def reaction_fixture(attrs \\ %{}) do 39 | {:ok, reaction} = 40 | attrs 41 | |> Enum.into(%{ 42 | icon: "some icon" 43 | }) 44 | |> Claper.Posts.create_reaction() 45 | 46 | reaction 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/support/fixtures/presentations_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.PresentationsFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Claper.Presentations` context. 5 | """ 6 | 7 | import Claper.{EventsFixtures} 8 | 9 | require Claper.UtilFixture 10 | 11 | @doc """ 12 | Generate a presentation_file. 13 | """ 14 | def presentation_file_fixture(attrs \\ %{}, preload \\ []) do 15 | assoc = %{event: attrs[:event] || event_fixture(attrs)} 16 | 17 | {:ok, presentation_file} = 18 | attrs 19 | |> Enum.into(%{ 20 | hash: "123456", 21 | length: 42, 22 | status: "done", 23 | event_id: assoc.event.id 24 | }) 25 | |> Claper.Presentations.create_presentation_file() 26 | 27 | Claper.UtilFixture.merge_preload(presentation_file, preload, assoc) 28 | end 29 | 30 | @doc """ 31 | Generate a presentation_state. 32 | """ 33 | def presentation_state_fixture(attrs \\ %{}, preload \\ []) do 34 | assoc = %{presentation_file: attrs[:presentation_file] || presentation_file_fixture()} 35 | 36 | {:ok, presentation_state} = 37 | attrs 38 | |> Enum.into(%{ 39 | presentation_file_id: assoc.presentation_file.id, 40 | position: 0, 41 | banned: [], 42 | chat_visible: false, 43 | poll_visible: false, 44 | join_screen_visible: false 45 | }) 46 | |> Claper.Presentations.create_presentation_state() 47 | 48 | Claper.UtilFixture.merge_preload(presentation_state, preload, assoc) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/support/fixtures/quizzes_fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.QuizzesFixtures do 2 | @moduledoc """ 3 | This module defines test helpers for creating 4 | entities via the `Claper.Quizzes` context. 5 | """ 6 | 7 | import Claper.PresentationsFixtures 8 | 9 | def quiz_fixture(attrs \\ %{}) do 10 | presentation_file = attrs[:presentation_file] || presentation_file_fixture() 11 | 12 | attrs = 13 | attrs 14 | |> Enum.into(%{ 15 | title: "some quiz title", 16 | position: 42, 17 | enabled: false, 18 | show_results: false, 19 | presentation_file_id: presentation_file.id, 20 | quiz_questions: [ 21 | %{ 22 | content: "some question content", 23 | type: "qcm", 24 | quiz_question_opts: [ 25 | %{ 26 | content: "option 1", 27 | is_correct: true 28 | }, 29 | %{ 30 | content: "option 2", 31 | is_correct: false 32 | } 33 | ] 34 | } 35 | ], 36 | lti_resource_id: nil, 37 | lti_line_item_url: nil 38 | }) 39 | 40 | case Claper.Quizzes.create_quiz(attrs) do 41 | {:ok, quiz} -> 42 | quiz 43 | 44 | {:error, changeset} -> 45 | raise "Failed to create quiz: #{inspect(changeset.errors)}" 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/support/util_fixture.ex: -------------------------------------------------------------------------------- 1 | defmodule Claper.UtilFixture do 2 | defmacro merge_preload(origin, preload, assoc) do 3 | quote do 4 | unquote(origin) 5 | |> Map.merge( 6 | for p <- unquote(preload), unquote(assoc)[p], into: %{}, do: {p, unquote(assoc)[p]} 7 | ) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(Claper.Repo, :manual) 3 | --------------------------------------------------------------------------------