├── .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 |
10 |
13 |
14 |
15 |
16 |
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 | VIDEO
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 |
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 | <%= gettext("Clear cookies (at least for Claper domain)") %>
18 | <%= gettext("Reload the page you're trying to access (don't re-submit data)") %>
19 | <%= gettext("Try logging in again") %>
20 | <%= gettext("Ensure the URL does not contain an extra \"/\" anywhere") %>
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 |
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 |
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 |
17 | <%= gettext("Close") %>
18 |
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 | <%= gettext("Activate the tool in your LMS") %>
15 | <%= gettext("Configure it to be opened in a new window") %>
16 |
17 | <%= gettext("Check the permissions to share name and email of users") %>
18 |
19 |
20 |
21 |
22 |
26 | <%= gettext("Finish") %>
27 |
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 |
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 |
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 |
36 | <%= gettext("Send link to reset password") %>
37 |
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 |
10 |
13 |
14 |
15 |
16 |
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 | "VIDEO ",
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 |
--------------------------------------------------------------------------------