22 |
23 | $(help_title_section Environments) [default: test]
24 | --test Run all tests in test environment.
25 | --dev Run all tests in dev environment.
26 |
27 | $(help_title_section Options)
28 | -h --help Show this screen.
29 | -v --version Show version.
30 | EOF
31 | }
32 |
33 | case ${1:---test} in
34 | -h | --help)
35 | display_help
36 | ;;
37 | -v | --version)
38 | display_version "${VERSION}" "${PROGRAM}"
39 | ;;
40 | --test)
41 | mix test
42 | ;;
43 | *)
44 | display_help >&2
45 | exit 1
46 | ;;
47 | esac
48 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # For production, don't forget to configure the url host
4 | # to something meaningful, Phoenix uses this information
5 | # when generating URLs.
6 | #
7 | # Note we also include the path to a cache manifest
8 | # containing the digested version of static files. This
9 | # manifest is generated by the `mix phx.digest` task,
10 | # which you should run after static files are built and
11 | # before starting your production server.
12 | config :atomic, AtomicWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
13 |
14 | # Do not print debug messages in production
15 | config :logger, level: :info
16 |
17 | # ## SSL Support
18 | #
19 | # To get SSL working, you will need to add the `https` key
20 | # to the previous section and set your `:url` port to 443:
21 | #
22 | # config :atomic, AtomicWeb.Endpoint,
23 | # ...,
24 | # url: [host: "example.com", port: 443],
25 | # https: [
26 | # ...,
27 | # port: 443,
28 | # cipher_suite: :strong,
29 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
30 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
31 | # ]
32 | #
33 | # The `cipher_suite` is set to `:strong` to support only the
34 | # latest and more secure SSL ciphers. This means old browsers
35 | # and clients may not be supported. You can set it to
36 | # `:compatible` for wider support.
37 | #
38 | # `:keyfile` and `:certfile` expect an absolute path to the key
39 | # and cert in disk or a relative path inside priv, for example
40 | # "priv/ssl/server.key". For all supported SSL configuration
41 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
42 | #
43 | # We also recommend setting `force_ssl` in your endpoint, ensuring
44 | # no data is ever sent via http, always redirecting to https:
45 | #
46 | # config :atomic, AtomicWeb.Endpoint,
47 | # force_ssl: [hsts: true]
48 | #
49 | # Check `Plug.SSL` for all available options in `force_ssl`.
50 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | # Only in tests, remove the complexity from the password hashing algorithm
4 | config :bcrypt_elixir, :log_rounds, 1
5 |
6 | # Configure your database
7 | #
8 | # The MIX_TEST_PARTITION environment variable can be used
9 | # to provide built-in test partitioning in CI environment.
10 | # Run `mix help test` for more information.
11 | config :atomic, Atomic.Repo,
12 | username: "postgres",
13 | password: "postgres",
14 | hostname: "localhost",
15 | database: "atomic_test#{System.get_env("MIX_TEST_PARTITION")}",
16 | pool: Ecto.Adapters.SQL.Sandbox,
17 | pool_size: 10
18 |
19 | # We don't run a server during test. If one is required,
20 | # you can enable the server option below.
21 | config :atomic, AtomicWeb.Endpoint,
22 | http: [ip: {127, 0, 0, 1}, port: 4002],
23 | secret_key_base: "r9M5TJmKSjEn4aRObrwewuqLRaMDW/J58cZTKs5ZpB+dHTyMb7jf7cg1eRXJ+73v",
24 | server: false
25 |
26 | # In test we don't send emails.
27 | config :atomic, Atomic.Mailer, adapter: Swoosh.Adapters.Test
28 |
29 | # Print only warnings and errors during test
30 | config :logger, level: :warn
31 |
32 | # Initialize plugs at runtime for faster test compilation
33 | config :phoenix, :plug_init_mode, :runtime
34 |
35 | # Other configurations for the app
36 | config :pdf_generator, raise_on_missing_wkhtmltopdf_binary: false
37 |
--------------------------------------------------------------------------------
/darwin.yml:
--------------------------------------------------------------------------------
1 | services:
2 | db:
3 | ports:
4 | - ${DB_PORT:-5555}:5432
5 | web:
6 | ports:
7 | - ${PORT:-4000}:4000
8 |
9 |
--------------------------------------------------------------------------------
/data/courses.txt:
--------------------------------------------------------------------------------
1 | Administração Pública
2 | Arqueologia
3 | Artes Visuais
4 | Biologia Aplicada
5 | Biologia e Geologia
6 | Bioquímica
7 | Ciência de Dados
8 | Ciências da Educação
9 | Ciência Política
10 | Ciências da Computação
11 | Ciências da Comunicação
12 | Ciências do Ambiente
13 | Contabilidade
14 | Criminologia e Justiça Criminal
15 | Design de Produto
16 | Design e Marketing de Moda
17 | Direito
18 | Economia
19 | Educação
20 | Educação Básica
21 | Enfermagem
22 | Engenharia Aeroespacial
23 | Engenharia Biomédica
24 | Engenharia Civil
25 | Engenharia de Materiais
26 | Engenharia de Polímeros
27 | Engenharia de Telecomunicações e Informática
28 | Engenharia e Gestão de Sistemas de Informação
29 | Engenharia e Gestão Industrial
30 | Engenharia Eletrónica Industrial e Computadores
31 | Engenharia Física
32 | Engenharia Informática
33 | Engenharia Mecânica
34 | Engenharia Química e Biológica
35 | Engenharia Têxtil
36 | Estatística Aplicada
37 | Estudos Culturais
38 | Estudos Orientais: Estudos Chineses e Japoneses
39 | Estudos Portugueses
40 | Filosofia
41 | Física
42 | Geografia e Planeamento
43 | Geologia
44 | Gestão
45 | História
46 | Línguas Aplicadas
47 | Línguas e Literaturas Europeias
48 | Medicina
49 | Marketing
50 | Matemática
51 | Música
52 | Negócios Internacionais
53 | Optometria e Ciências da Visão
54 | Proteção Civil e Gestão do Território
55 | Psicologia
56 | Química
57 | Relações Internacionais
58 | Sociologia
59 | Teatro
60 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | services:
2 | db:
3 | image: postgres:14.1
4 | container_name: atomic_db
5 | env_file: .env.dev
6 | environment:
7 | POSTGRES_USER: ${DB_USERNAME:-postgres}
8 | POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
9 | POSTGRES_HOST: ${DB_HOST:-localhost}
10 | volumes:
11 | - /var/lib/postgresql/data
12 | web:
13 | container_name: atomic_web
14 | env_file: .env.dev
15 | environment:
16 | MIX_ENV: ${MIX_ENV:-dev}
17 | build:
18 | context: .
19 | dockerfile: Dockerfile.dev
20 | depends_on:
21 | - db
22 | volumes:
23 | - ./:/app
24 | - /app/_build
25 | - /app/deps
26 | - /app/priv/uploads
27 |
--------------------------------------------------------------------------------
/lib/atomic.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic do
2 | @moduledoc """
3 | Atomic 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/atomic/accounts/course.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Accounts.Course do
2 | @moduledoc """
3 | A course the user is enrolled in.
4 | """
5 | use Atomic.Schema
6 |
7 | alias Atomic.Accounts.User
8 |
9 | @required_fields ~w(name cycle)a
10 | @cycles ~w(Bachelors Masters PhD)a
11 |
12 | schema "courses" do
13 | field :name, :string
14 | field :cycle, Ecto.Enum, values: @cycles
15 |
16 | has_many :users, User
17 |
18 | timestamps()
19 | end
20 |
21 | @doc false
22 | def changeset(course, attrs) do
23 | course
24 | |> cast(attrs, @required_fields)
25 | |> validate_required(@required_fields)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/atomic/accounts/user_notifier.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Accounts.UserNotifier do
2 | @moduledoc false
3 | import Swoosh.Email
4 |
5 | alias Atomic.Mailer
6 |
7 | use Phoenix.Swoosh, view: AtomicWeb.EmailView
8 |
9 | defp base_email(to: email) do
10 | new()
11 | |> to(email)
12 | |> from({"Atomic", "noreply@atomic.cesium.pt"})
13 | end
14 |
15 | defp deliver(recipient, subject, body) do
16 | email =
17 | new()
18 | |> to(recipient)
19 | |> from({"Atomic", "contact@example.com"})
20 | |> subject(subject)
21 | |> text_body(body)
22 |
23 | with {:ok, _metadata} <- Mailer.deliver(email) do
24 | {:ok, email}
25 | end
26 | end
27 |
28 | @doc """
29 | Deliver instructions to confirm account.
30 | """
31 | def deliver_confirmation_instructions(user, url) do
32 | email =
33 | base_email(to: user.email)
34 | |> subject("Confirm your Account")
35 | |> assign(:user, user)
36 | |> assign(:url, url)
37 | |> render_body("user_confirmation.html")
38 |
39 | case Mailer.deliver(email) do
40 | {:ok, _term} -> {:ok, email}
41 | {:error, ch} -> {:error, ch}
42 | end
43 | end
44 |
45 | @doc """
46 | Deliver instructions to reset a user password.
47 | """
48 | def deliver_reset_password_instructions(user, url) do
49 | email =
50 | base_email(to: user.email)
51 | |> subject("Reset Password Instructions")
52 | |> assign(:user, user)
53 | |> assign(:url, url)
54 | |> render_body("user_reset_password.html")
55 |
56 | case Mailer.deliver(email) do
57 | {:ok, _term} -> {:ok, email}
58 | {:error, ch} -> {:error, ch}
59 | end
60 | end
61 |
62 | @doc """
63 | Deliver instructions to update a user email.
64 | """
65 | def deliver_update_email_instructions(user, url) do
66 | deliver(user.email, "Update email instructions", """
67 |
68 | ==============================
69 |
70 | Hi #{user.email},
71 |
72 | You can change your email by visiting the URL below:
73 |
74 | #{url}
75 |
76 | If you didn't request this change, please ignore this.
77 |
78 | ==============================
79 | """)
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/atomic/activities/enrollment.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Activities.Enrollment do
2 | @moduledoc """
3 | An activity enrollment.
4 | """
5 | use Atomic.Schema
6 |
7 | alias Atomic.Accounts.User
8 | alias Atomic.Activities
9 | alias Atomic.Activities.Activity
10 |
11 | @required_fields ~w(activity_id user_id)a
12 | @optional_fields ~w(present)a
13 |
14 | schema "enrollments" do
15 | field :present, :boolean, default: false
16 |
17 | belongs_to :activity, Activity
18 | belongs_to :user, User
19 |
20 | timestamps()
21 | end
22 |
23 | def changeset(enrollment, attrs) do
24 | enrollment
25 | |> cast(attrs, @required_fields ++ @optional_fields)
26 | |> validate_maximum_entries()
27 | |> validate_required(@required_fields)
28 | end
29 |
30 | def update_changeset(enrollment, attrs) do
31 | enrollment
32 | |> cast(attrs, @required_fields ++ @optional_fields)
33 | |> validate_required(@required_fields)
34 | end
35 |
36 | defp validate_maximum_entries(changeset) do
37 | activity_id = get_field(changeset, :activity_id)
38 | activity = Activities.get_activity!(activity_id)
39 |
40 | if activity.maximum_entries <= activity.enrolled do
41 | add_error(changeset, :activity_id, gettext("maximum number of enrollments reached"))
42 | else
43 | changeset
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/atomic/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.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 | children = [
11 | # Start the Ecto repository
12 | Atomic.Repo,
13 | # Start the Telemetry supervisor
14 | AtomicWeb.Telemetry,
15 | # Start the PubSub system
16 | {Phoenix.PubSub, name: Atomic.PubSub},
17 | # Start the Endpoint (http/https)
18 | AtomicWeb.Endpoint,
19 | # Start the scheduler
20 | Atomic.Scheduler
21 | # Start a worker by calling: Atomic.Worker.start_link(arg)
22 | # {Atomic.Worker, arg}
23 | ]
24 |
25 | # See https://hexdocs.pm/elixir/Supervisor.html
26 | # for other strategies and supported options
27 | opts = [strategy: :one_for_one, name: Atomic.Supervisor]
28 | Supervisor.start_link(children, opts)
29 | end
30 |
31 | # Tell Phoenix to update the endpoint configuration
32 | # whenever the application is updated.
33 | @impl true
34 | def config_change(changed, _new, removed) do
35 | AtomicWeb.Endpoint.config_change(changed, removed)
36 | :ok
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/atomic/context.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Context do
2 | @moduledoc """
3 | A utility context providing common functions to all context modules.
4 | """
5 | defmacro __using__(_) do
6 | quote do
7 | import Ecto.Query, warn: false
8 |
9 | alias Atomic.Repo
10 | alias Ecto.Multi
11 |
12 | def apply_filters(query, opts) do
13 | Enum.reduce(opts, query, fn
14 | {:where, filters}, query ->
15 | where(query, ^filters)
16 |
17 | {:fields, fields}, query ->
18 | select(query, [i], map(i, ^fields))
19 |
20 | {:order_by, criteria}, query ->
21 | order_by(query, ^criteria)
22 |
23 | {:limit, criteria}, query ->
24 | limit(query, ^criteria)
25 |
26 | {:offset, criteria}, query ->
27 | offset(query, ^criteria)
28 |
29 | {:preloads, preloads}, query when is_list(preloads) ->
30 | Enum.reduce(preloads, query, fn preload, query ->
31 | preload(query, ^preload)
32 | end)
33 |
34 | {:preloads, preload}, query ->
35 | preload(query, ^preload)
36 |
37 | _, query ->
38 | query
39 | end)
40 | end
41 |
42 | defp after_save({:ok, data}, func) do
43 | {:ok, _data} = func.(data)
44 | end
45 |
46 | defp after_save(error, _func), do: error
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/atomic/feed/post.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Feed.Post do
2 | @moduledoc """
3 | A post published in the feed. Can either be an announcement or an activity.
4 | """
5 | use Atomic.Schema
6 |
7 | alias Atomic.Activities.Activity
8 | alias Atomic.Organizations.Announcement
9 |
10 | @types ~w(activity announcement)a
11 |
12 | @required_fields ~w(type)a
13 |
14 | schema "posts" do
15 | field :type, Ecto.Enum, values: @types
16 |
17 | has_one :activity, Activity
18 | has_one :announcement, Announcement
19 |
20 | timestamps()
21 | end
22 |
23 | def changeset(post, attrs) do
24 | post
25 | |> cast(attrs, @required_fields)
26 | |> validate_required(@required_fields)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/atomic/generate_avatar.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.GenerateAvatar do
2 | @moduledoc """
3 | A module for generating unique, GitHub-style avatars for organizations.
4 | """
5 |
6 | import Phoenix.HTML
7 |
8 | @grid_size 5
9 | @cell_size 50
10 |
11 | def generate_avatar(seed, output_type) do
12 | hash = :crypto.hash(:sha256, seed) |> :binary.bin_to_list()
13 | color = Enum.take(hash, 3)
14 | grid = build_grid(hash)
15 | svg = draw(grid, color)
16 |
17 | handle_output(svg, output_type)
18 | end
19 |
20 | defp handle_output(svg, output) when is_binary(output), do: File.write(output, svg)
21 |
22 | defp handle_output(svg, :svg), do: svg
23 | defp handle_output(svg, :blob), do: :erlang.term_to_binary(svg)
24 | defp handle_output(svg, :html), do: raw(svg)
25 |
26 | defp handle_output(_svg, invalid) do
27 | raise ArgumentError,
28 | "Invalid output type: #{inspect(invalid)}. Expected one of :svg, :blob, :html, or a file path string."
29 | end
30 |
31 | defp build_grid(hash) do
32 | hash
33 | |> Enum.chunk_every(@grid_size, @grid_size, :discard)
34 | |> Enum.map(&mirror/1)
35 | |> List.flatten()
36 | end
37 |
38 | defp mirror([a, b, c | _]), do: [a, b, c, b, a]
39 |
40 | defp draw(grid, [r, g, b]) do
41 | header = """
42 |
43 | """
44 |
45 | footer = " "
46 |
47 | body =
48 | Enum.map_join(
49 | grid
50 | |> Enum.with_index()
51 | |> Enum.filter(fn {val, _} -> rem(val, 2) == 0 end),
52 | "\n",
53 | fn {_val, index} ->
54 | x = rem(index, @grid_size) * @cell_size
55 | y = div(index, @grid_size) * @cell_size
56 |
57 | " "
58 | end
59 | )
60 |
61 | header <> body <> footer
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/atomic/location/location.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Location do
2 | @moduledoc """
3 | A location embedded struct schema.
4 | """
5 | use Atomic.Schema
6 |
7 | @required_fields ~w(name)a
8 | @optional_fields ~w(url)a
9 |
10 | @derive Jason.Encoder
11 | @primary_key false
12 | embedded_schema do
13 | field :name, :string
14 | field :url, :string
15 | end
16 |
17 | def changeset(location, attrs) do
18 | location
19 | |> cast(attrs, @required_fields ++ @optional_fields)
20 | |> validate_required(@required_fields)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/atomic/mailer.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Mailer do
2 | @moduledoc false
3 | use Swoosh.Mailer, otp_app: :atomic
4 | end
5 |
--------------------------------------------------------------------------------
/lib/atomic/organizations/announcement.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Organizations.Announcement do
2 | @moduledoc """
3 | An announcement created and published by an organization.
4 | """
5 | use Atomic.Schema
6 |
7 | alias Atomic.Feed.Post
8 | alias Atomic.Organizations.Organization
9 |
10 | @required_fields ~w(title description organization_id)a
11 | @optional_fields ~w()a
12 |
13 | @derive {
14 | Flop.Schema,
15 | filterable: [],
16 | sortable: [:inserted_at],
17 | default_order: %{
18 | order_by: [:inserted_at],
19 | order_directions: [:desc]
20 | }
21 | }
22 |
23 | schema "announcements" do
24 | field :title, :string
25 | field :description, :string
26 | field :image, Uploaders.Post.Type
27 |
28 | belongs_to :organization, Organization
29 | belongs_to :post, Post, foreign_key: :post_id
30 |
31 | timestamps()
32 | end
33 |
34 | def changeset(announcements, attrs) do
35 | announcements
36 | |> cast(attrs, @required_fields ++ @optional_fields)
37 | |> validate_required(@required_fields)
38 | end
39 |
40 | def image_changeset(announcement, attrs) do
41 | announcement
42 | |> cast_attachments(attrs, [:image])
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/atomic/organizations/collaborator.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Organizations.Collaborator do
2 | @moduledoc """
3 | A relation representing an organization department collaborator.
4 | """
5 | use Atomic.Schema
6 |
7 | alias Atomic.Accounts.User
8 | alias Atomic.Organizations.Department
9 |
10 | @required_fields ~w(user_id department_id accepted)a
11 | @optional_fields ~w(accepted_at)a
12 |
13 | @derive {
14 | Flop.Schema,
15 | default_limit: 7,
16 | filterable: [:accepted],
17 | sortable: [:collaborator_name, :inserted_at, :updated_at],
18 | default_order: %{
19 | order_by: [:inserted_at],
20 | order_directions: [:desc]
21 | },
22 | adapter_opts: [
23 | join_fields: [
24 | collaborator_name: [binding: :user, field: :name, path: [:user, :name]]
25 | ]
26 | ]
27 | }
28 |
29 | schema "collaborators" do
30 | belongs_to :user, User
31 | belongs_to :department, Department
32 |
33 | field :accepted, :boolean, default: false
34 | field :accepted_at, :naive_datetime
35 |
36 | timestamps()
37 | end
38 |
39 | def changeset(collaborator_departments, attrs) do
40 | collaborator_departments
41 | |> cast(attrs, @required_fields ++ @optional_fields)
42 | |> validate_required(@required_fields)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/atomic/organizations/department.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Organizations.Department do
2 | @moduledoc """
3 | A department of an organization.
4 | """
5 | use Atomic.Schema
6 |
7 | alias Atomic.Organizations.Organization
8 |
9 | @required_fields ~w(name organization_id)a
10 | @optional_fields ~w(description collaborator_applications archived)a
11 |
12 | schema "departments" do
13 | field :name, :string
14 | field :description, :string
15 |
16 | field :collaborator_applications, :boolean, default: false
17 | field :archived, :boolean, default: false
18 |
19 | field :banner, Atomic.Uploaders.Banner.Type
20 |
21 | belongs_to :organization, Organization, on_replace: :delete_if_exists
22 |
23 | timestamps()
24 | end
25 |
26 | def changeset(department, attrs) do
27 | department
28 | |> cast(attrs, @required_fields ++ @optional_fields)
29 | |> validate_required(@required_fields)
30 | end
31 |
32 | def banner_changeset(department, attrs) do
33 | department
34 | |> cast_attachments(attrs, [:banner])
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/atomic/organizations/membership.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Organizations.Membership do
2 | @moduledoc """
3 | Schema representing a user's membership in an organization.
4 |
5 | Memberships are used to track the relationship between a user and an organization.
6 |
7 | Types of memberships:
8 | * `owner` - The user has full control over the organization.
9 | * `admin` - The user can control the organization's departments, activities and partners.
10 | * `follower` - The user is following the organization.
11 |
12 | This schema can be further extended to include additional roles, such as `member`.
13 | """
14 | use Atomic.Schema
15 |
16 | alias Atomic.Accounts.User
17 | alias Atomic.Organizations.Organization
18 |
19 | @required_fields ~w(user_id organization_id role)a
20 | @optional_fields ~w()a
21 |
22 | @roles ~w(follower admin owner)a
23 |
24 | schema "memberships" do
25 | field :role, Ecto.Enum, values: @roles
26 |
27 | belongs_to :user, User
28 | belongs_to :organization, Organization
29 |
30 | timestamps()
31 | end
32 |
33 | def changeset(organization, attrs) do
34 | organization
35 | |> cast(attrs, @required_fields ++ @optional_fields)
36 | |> validate_required(@required_fields)
37 | end
38 |
39 | def roles, do: @roles
40 | end
41 |
--------------------------------------------------------------------------------
/lib/atomic/organizations/organization.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Organizations.Organization do
2 | @moduledoc false
3 | use Atomic.Schema
4 |
5 | alias Atomic.Accounts.User
6 | alias Atomic.Location
7 | alias Atomic.Organizations.{Announcement, Department, Membership, Partner}
8 | alias Atomic.Uploaders
9 |
10 | @required_fields ~w(name long_name description)a
11 | @optional_fields ~w()a
12 |
13 | @derive {
14 | Flop.Schema,
15 | filterable: [],
16 | sortable: [:name],
17 | compound_fields: [search: [:name]],
18 | default_order: %{
19 | order_by: [:name],
20 | order_directions: [:asc]
21 | }
22 | }
23 |
24 | schema "organizations" do
25 | field :name, :string
26 | field :long_name, :string
27 | field :description, :string
28 |
29 | field :logo, Uploaders.Logo.Type
30 | embeds_one :location, Location, on_replace: :delete
31 |
32 | has_many :departments, Department,
33 | on_replace: :delete_if_exists,
34 | on_delete: :delete_all,
35 | preload_order: [asc: :name]
36 |
37 | has_many :partners, Partner,
38 | on_replace: :delete_if_exists,
39 | on_delete: :delete_all,
40 | preload_order: [asc: :name]
41 |
42 | has_many :announcements, Announcement,
43 | on_replace: :delete,
44 | preload_order: [asc: :inserted_at]
45 |
46 | many_to_many :users, User, join_through: Membership
47 |
48 | timestamps()
49 | end
50 |
51 | def changeset(organization, attrs) do
52 | organization
53 | |> cast(attrs, @required_fields ++ @optional_fields)
54 | |> cast_embed(:location, with: &Location.changeset/2)
55 | |> validate_required(@required_fields)
56 | |> unique_constraint(:name)
57 | end
58 |
59 | def logo_changeset(organization, attrs) do
60 | organization
61 | |> cast_attachments(attrs, [:logo])
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/atomic/organizations/partner.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Organizations.Partner do
2 | @moduledoc """
3 | Schema representing a partner of an organization.
4 | """
5 | use Atomic.Schema
6 |
7 | alias Atomic.Location
8 | alias Atomic.Organizations.Organization
9 | alias Atomic.Socials
10 |
11 | @required_fields ~w(name organization_id)a
12 | @optional_fields ~w(description benefits archived image notes)a
13 |
14 | @derive {
15 | Flop.Schema,
16 | filterable: [],
17 | sortable: [:name],
18 | compound_fields: [search: [:name]],
19 | default_order: %{
20 | order_by: [:name],
21 | order_directions: [:asc]
22 | }
23 | }
24 |
25 | schema "partners" do
26 | field :name, :string
27 | field :description, :string
28 | field :notes, :string
29 |
30 | field :benefits, :string
31 | field :archived, :boolean, default: false
32 | field :image, Uploaders.PartnerImage.Type
33 |
34 | embeds_one :location, Location, on_replace: :update
35 | embeds_one :socials, Socials, on_replace: :update
36 |
37 | belongs_to :organization, Organization
38 |
39 | timestamps()
40 | end
41 |
42 | def changeset(partner, attrs) do
43 | partner
44 | |> cast(attrs, @required_fields ++ @optional_fields)
45 | |> cast_embed(:location, with: &Location.changeset/2)
46 | |> cast_embed(:socials, with: &Socials.changeset/2)
47 | |> validate_required(@required_fields)
48 | |> unique_constraint(:name)
49 | end
50 |
51 | def image_changeset(partner, attrs) do
52 | partner
53 | |> cast_attachments(attrs, [:image])
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/atomic/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo do
2 | use Ecto.Repo,
3 | otp_app: :atomic,
4 | adapter: Ecto.Adapters.Postgres
5 |
6 | use Paginator
7 | end
8 |
--------------------------------------------------------------------------------
/lib/atomic/scheduler.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Scheduler do
2 | @moduledoc false
3 | use Quantum, otp_app: :atomic
4 | end
5 |
--------------------------------------------------------------------------------
/lib/atomic/schema.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Schema do
2 | @moduledoc """
3 | The application Schema for all the modules, providing Ecto.UUIDs as default id.
4 | """
5 | use Gettext, backend: AtomicWeb.Gettext
6 |
7 | alias Atomic.Time
8 |
9 | defmacro __using__(_) do
10 | quote do
11 | use Ecto.Schema
12 | use Waffle.Ecto.Schema
13 | use Gettext, backend: AtomicWeb.Gettext
14 |
15 | import Ecto.Changeset
16 | import Ecto.Query
17 |
18 | alias Atomic.Uploaders
19 |
20 | @primary_key {:id, :binary_id, autogenerate: true}
21 | @foreign_key_type :binary_id
22 |
23 | def validate_email_address(changeset, field) do
24 | changeset
25 | |> validate_format(
26 | field,
27 | ~r/^[\w.!#$%&’*+\-\/=?\^`{|}~]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/i,
28 | message: gettext("must be a valid email")
29 | )
30 | end
31 |
32 | def validate_naive_datetime(changeset, field, :future) do
33 | validate_change(changeset, field, fn _field, value ->
34 | if NaiveDateTime.compare(value, Time.lisbon_now()) == :lt do
35 | [{field, gettext("date in the past")}]
36 | else
37 | []
38 | end
39 | end)
40 | end
41 |
42 | def validate_naive_datetime(changeset, field, date) do
43 | validate_change(changeset, field, fn _field, value ->
44 | if NaiveDateTime.compare(value, date) == :lt do
45 | [{field, gettext("date requires to be after %{date}", date: date)}]
46 | else
47 | []
48 | end
49 | end)
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/atomic/socials/socials.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Socials do
2 | @moduledoc """
3 | A socials embedded struct schema.
4 | """
5 | use Atomic.Schema
6 |
7 | @optional_fields ~w(instagram facebook x youtube tiktok website)a
8 |
9 | @derive Jason.Encoder
10 | @primary_key false
11 | embedded_schema do
12 | field :instagram, :string
13 | field :facebook, :string
14 | field :x, :string
15 | field :youtube, :string
16 | field :tiktok, :string
17 | field :website, :string
18 | end
19 |
20 | def changeset(socials, attrs) do
21 | socials
22 | |> cast(attrs, @optional_fields)
23 | |> validate_format(:website, ~r{^https?://}, message: "must start with http:// or https://")
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/atomic/time.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Time do
2 | @moduledoc """
3 | This module provide time utilities.
4 | """
5 |
6 | @timezone "Europe/Lisbon"
7 |
8 | def lisbon_now do
9 | Timex.now()
10 | |> Timex.Timezone.convert(timezone())
11 | end
12 |
13 | def convert_to_lisbon(datetime) do
14 | Timex.Timezone.convert(datetime, timezone())
15 | end
16 |
17 | defp timezone do
18 | Timex.Timezone.get(@timezone)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/atomic/uploader.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Uploader do
2 | @moduledoc """
3 | A utility module providing common functions to all uploaders modules.
4 | Put `use Atomic.Uploader` on top of your uploader module to use it.
5 | """
6 |
7 | defmacro __using__(opts) do
8 | quote do
9 | use Waffle.Definition
10 | use Waffle.Ecto.Definition
11 |
12 | def validate({file, _}) do
13 | file_extension = file.file_name |> Path.extname() |> String.downcase()
14 | size = file_size(file)
15 |
16 | case Enum.member?(extension_whitelist(), file_extension) do
17 | true ->
18 | if size <= max_size() do
19 | :ok
20 | else
21 | {:error, "file size exceeds maximum allowed size"}
22 | end
23 |
24 | false ->
25 | {:error, "invalid file extension"}
26 | end
27 | end
28 |
29 | def extension_whitelist do
30 | Keyword.get(unquote(opts), :extensions, [])
31 | end
32 |
33 | def max_size do
34 | Keyword.get(unquote(opts), :max_file_size, 100_000_000)
35 | end
36 |
37 | def file_size(%Waffle.File{} = file) do
38 | File.stat!(file.path) |> Map.get(:size)
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/atomic/uploaders/banner.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Uploaders.Banner do
2 | @moduledoc """
3 | Uploader for user banners.
4 | """
5 | use Atomic.Uploader, extensions: ~w(.jpg .jpeg .png .gif)
6 | alias Atomic.Accounts.User
7 |
8 | @versions [:original]
9 |
10 | def storage_dir(_version, {_file, %User{} = user}) do
11 | "uploads/atomic/users/#{user.id}/banner"
12 | end
13 |
14 | def filename(version, _) do
15 | version
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/atomic/uploaders/logo.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Uploaders.Logo do
2 | @moduledoc """
3 | Uploader for organization logos.
4 | """
5 | use Atomic.Uploader, extensions: ~w(.jpg .jpeg .png .svg)
6 | alias Atomic.Organizations.Organization
7 |
8 | @versions [:original]
9 |
10 | def storage_dir(_version, {_file, %Organization{} = organization}) do
11 | "uploads/atomic/organizations/#{organization.id}/logo"
12 | end
13 |
14 | def filename(version, _) do
15 | version
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/atomic/uploaders/partner_image.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Uploaders.PartnerImage do
2 | @moduledoc """
3 | Uploader for partner images.
4 | """
5 | use Atomic.Uploader, extensions: ~w(.jpg .jpeg .png)
6 |
7 | alias Atomic.Organizations.Partner
8 |
9 | @versions [:original]
10 |
11 | def storage_dir(_version, {_file, %Partner{} = partner}) do
12 | "uploads/atomic/partners/#{partner.id}/logo"
13 | end
14 |
15 | def filename(version, _) do
16 | version
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/atomic/uploaders/post.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Uploaders.Post do
2 | @moduledoc """
3 | Uploader for posts.
4 | """
5 | use Atomic.Uploader, extensions: ~w(.jpg .jpeg .png .svg)
6 |
7 | alias Atomic.Activities.Activity
8 | alias Atomic.Organizations.Announcement
9 |
10 | @versions [:original]
11 |
12 | def storage_dir(_version, {_file, %Activity{} = activity}) do
13 | "uploads/atomic/activities/#{activity.id}/image"
14 | end
15 |
16 | def storage_dir(_version, {_file, %Announcement{} = announcement}) do
17 | "uploads/atomic/announcements/#{announcement.id}/image"
18 | end
19 |
20 | def filename(version, _) do
21 | version
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/atomic/uploaders/profile_picture.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Uploaders.ProfilePicture do
2 | @moduledoc """
3 | Uploader for profile pictures.
4 | """
5 | use Atomic.Uploader, extensions: ~w(.jpg .jpeg .png .gif)
6 | alias Atomic.Accounts.User
7 |
8 | @versions [:original]
9 |
10 | def storage_dir(_version, {_file, %User{} = user}) do
11 | "uploads/atomic/users/#{user.id}/profile_picture"
12 | end
13 |
14 | def filename(version, _) do
15 | version
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/announcement.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.Announcement do
2 | @moduledoc """
3 | Renders an announcement.
4 | """
5 | use AtomicWeb, :component
6 |
7 | import AtomicWeb.Components.Avatar
8 |
9 | attr :announcement, :map, required: true, doc: "The announcement to render."
10 |
11 | def announcement(assigns) do
12 | ~H"""
13 |
14 |
15 |
16 | <.avatar name={@announcement.organization.name} color={:light_zinc} class="!h-10 !w-10" size={:xs} type={:organization} src={Uploaders.Logo.url({@announcement.organization.logo, @announcement.organization}, :original)} />
17 |
18 |
19 |
20 | <.link navigate={~p"/organizations/#{@announcement.organization.id}"} class="hover:underline focus:outline-none">
21 |
22 | {@announcement.organization.name}
23 |
24 |
25 |
26 |
27 | Published on
28 | {relative_datetime(@announcement.inserted_at)}
29 |
30 |
31 |
32 |
{@announcement.title}
33 |
34 | {maybe_slice_string(@announcement.description, 300)}
35 |
36 |
37 | <%= if @announcement.image do %>
38 |
39 |
40 |
41 | <% end %>
42 |
43 | """
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/badge.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.Badge do
2 | @moduledoc false
3 | use Phoenix.Component
4 |
5 | import AtomicWeb.Components.Icon
6 |
7 | attr :size, :atom,
8 | default: :md,
9 | values: [:xs, :sm, :md, :lg, :xl],
10 | doc: "The size of the badge."
11 |
12 | attr :variant, :atom,
13 | default: :light,
14 | values: [:light, :dark, :outline],
15 | doc: "The variant of the badge."
16 |
17 | attr :color, :atom,
18 | default: :primary,
19 | values: [:primary, :secondary, :info, :success, :warning, :danger, :zinc],
20 | doc: "Badge color."
21 |
22 | attr :icon_position, :atom,
23 | values: [:left, :right],
24 | default: :left,
25 | doc: "The position of the icon if applicable."
26 |
27 | attr :icon, :string, default: nil, doc: "The icon to display."
28 | attr :icon_class, :string, default: "", doc: "Additional classes to apply to the icon."
29 |
30 | attr :class, :string, default: "", doc: "Additional classes to apply to the badge."
31 | attr :label, :string, default: nil, doc: "Badge label."
32 |
33 | attr :rest, :global,
34 | include:
35 | ~w(csrf_token disabled download form href hreflang method name navigate patch referrerpolicy rel replace target type value autofocus tabindex),
36 | doc: "Arbitrary HTML or phx attributes."
37 |
38 | slot :inner_block, required: false, doc: "Slot for the content of the badge."
39 |
40 | def badge(assigns) do
41 | ~H"""
42 |
51 | <%= if @icon && @icon_position == :left do %>
52 | <.icon name={@icon} class={"#{generate_icon_classes(assigns)}"} />
53 | <% end %>
54 | {render_slot(@inner_block) || @label}
55 | <%= if @icon && @icon_position == :right do %>
56 | <.icon name={@icon} class={"#{generate_icon_classes(assigns)}"} />
57 | <% end %>
58 |
59 | """
60 | end
61 |
62 | defp generate_icon_classes(assigns) do
63 | [
64 | "atomic-button__icon--#{assigns.size}",
65 | assigns.icon_class
66 | ]
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/empty.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.Empty do
2 | @moduledoc """
3 | A component for displaying an empty state.
4 | """
5 | use AtomicWeb, :component
6 |
7 | alias Inflex
8 |
9 | attr :id, :string, default: "empty-state", required: false
10 | attr :placeholder, :string, required: true
11 | attr :url, :string, required: true
12 |
13 | def empty_state(assigns) do
14 | ~H"""
15 |
16 | <.icon name="hero-plus-circle" class="size-12 mx-auto text-zinc-400" />
17 |
No {plural(@placeholder)}
18 |
Get started by creating a new {@placeholder}.
19 |
20 | <.link navigate={@url} class="bg-primary-500 inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-primary-600 focus-visible:outline-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2">
21 |
22 |
23 |
24 | New {@placeholder}
25 |
26 |
27 |
28 | """
29 | end
30 |
31 | # Returns the plural form of a word.
32 | @spec plural(String.t()) :: String.t()
33 | defp plural(word), do: Inflex.pluralize(word)
34 | end
35 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/gradient.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.Gradient do
2 | @moduledoc """
3 | Generates a random gradient background or a predictable gradient background based on a seed that can be of any data type.
4 | """
5 | use Phoenix.Component
6 |
7 | # List of gradients
8 | @colors [
9 | {"#000046", "#1CB5E0"},
10 | {"#007991", "#78ffd6"},
11 | {"#30E8BF", "#FF8235"},
12 | {"#C33764", "#1D2671"},
13 | {"#34e89e", "#0f3443"},
14 | {"#44A08D", "#093637"},
15 | {"#DCE35B", "#45B649"},
16 | {"#c0c0aa", "#1cefff"},
17 | {"#ee0979", "#ff6a00"}
18 | ]
19 |
20 | attr :class, :string, default: "", doc: "Additional classes to apply to the component."
21 | attr :seed, :any, required: false, doc: "For predictable gradients."
22 |
23 | def gradient(assigns) do
24 | {gradient_color_a, gradient_color_b} =
25 | if Map.has_key?(assigns, :seed) do
26 | generate_color(assigns.seed)
27 | else
28 | generate_color()
29 | end
30 |
31 | assigns
32 | |> assign(:gradient_color_a, gradient_color_a)
33 | |> assign(:gradient_color_b, gradient_color_b)
34 | |> render_gradient()
35 | end
36 |
37 | defp render_gradient(assigns) do
38 | ~H"""
39 |
40 | """
41 | end
42 |
43 | defp generate_color(seed) when is_binary(seed) do
44 | # Convert the argument into an integer
45 | index = :erlang.phash2(seed, length(@colors))
46 |
47 | # Return the chosen color
48 | Enum.at(@colors, index)
49 | end
50 |
51 | defp generate_color do
52 | Enum.random(@colors)
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/icon.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.Icon do
2 | @moduledoc """
3 | A component for rendering icons.
4 |
5 | An icon can either be from the [Heroicons](https://heroicons.com) or [Tabler Icons](https://tablericons.com) set.
6 | """
7 | use Phoenix.Component
8 |
9 | attr :name, :string, required: true
10 | attr :class, :string, default: nil
11 |
12 | def icon(%{name: "hero-" <> _} = assigns) do
13 | ~H"""
14 |
15 | """
16 | end
17 |
18 | def icon(%{name: "tabler-" <> _} = assigns) do
19 | ~H"""
20 |
21 | """
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/legal_pages_links.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.LegalPagesLinks do
2 | @moduledoc """
3 | Contains the structure for the legal pages navigation
4 | """
5 | use AtomicWeb, :component
6 |
7 | def legal_pages_links(assigns) do
8 | ~H"""
9 |
10 | <.link navigate={~p"/tos"} class="shrink-0 select-none">
11 |
{gettext("Terms of Service")}
12 |
13 | <.link navigate={~p"/privacy"} class="shrink-0 select-none">
14 |
{gettext("Privacy Policy")}
15 |
16 | <.link navigate={~p"/cookies"} class="shrink-0 select-none">
17 |
{gettext("Cookie Policy")}
18 |
19 |
© 2025 CeSIUM
20 |
21 | """
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/page.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.Page do
2 | @moduledoc """
3 | Component for the main page layout.
4 | """
5 | use Phoenix.Component
6 |
7 | attr :title, :string, required: true, doc: "The title of the page."
8 |
9 | attr :bottom_border, :boolean,
10 | default: false,
11 | doc: "Whether to show a bottom border after the page header."
12 |
13 | slot :actions, required: false, doc: "Slot for actions to be rendered in the page header."
14 | slot :inner_block, required: false, doc: "Slot for the body content of the page."
15 |
16 | def page(assigns) do
17 | ~H"""
18 |
19 |
20 |
21 |
22 |
23 |
24 | {@title}
25 |
26 | {render_slot(@actions)}
27 |
28 |
29 |
30 | {render_slot(@inner_block)}
31 |
32 |
33 |
34 | """
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/socials.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.Socials do
2 | @moduledoc false
3 |
4 | use AtomicWeb, :component
5 |
6 | attr :entity, :map, required: true
7 |
8 | def socials(assigns) do
9 | assigns = assign(assigns, :socials_with_values, get_social_values(assigns.entity))
10 |
11 | ~H"""
12 |
13 | <%= for {social, icon, url_base, social_value} <- assigns.socials_with_values do %>
14 | <%= if social_value do %>
15 |
16 |
icon} class="h-5 w-5" alt={Atom.to_string(social)} />
17 | <.link class="capitalize text-blue-500" target="_blank" href={url_base <> social_value}>
18 | {Atom.to_string(social)}
19 |
20 |
21 | <% end %>
22 | <% end %>
23 |
24 | """
25 | end
26 |
27 | defp get_social_values(entity) do
28 | socials = Map.get(entity, :socials, %{})
29 |
30 | get_socials()
31 | |> Enum.map(fn {social, icon, url_base} ->
32 | social_value = Map.get(socials, social)
33 | {social, icon, url_base, social_value}
34 | end)
35 | end
36 |
37 | def get_socials do
38 | [
39 | {:tiktok, "tiktok.svg", "https://tiktok.com/"},
40 | {:instagram, "instagram.svg", "https://instagram.com/"},
41 | {:facebook, "facebook.svg", "https://facebook.com/"},
42 | {:x, "x.svg", "https://x.com/"}
43 | ]
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/spinner.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.Spinner do
2 | @moduledoc false
3 | use Phoenix.Component
4 |
5 | attr :size, :atom,
6 | values: [:xs, :sm, :md, :lg, :xl],
7 | default: :sm,
8 | doc: "The size of the spinner."
9 |
10 | attr :show, :boolean, default: true, doc: "Show or hide spinner."
11 |
12 | attr :size_class, :string, default: nil, doc: "Custom CSS classes for size. eg: size-4"
13 |
14 | attr :class, :string, default: "", doc: "Additional classes to apply to the component."
15 |
16 | attr :rest, :global
17 |
18 | def spinner(assigns) do
19 | ~H"""
20 |
21 |
22 |
23 |
24 | """
25 | end
26 |
27 | defp generate_classes(assigns) do
28 | size_classes = assigns.size_class || "atomic-spinner--#{assigns.size}"
29 |
30 | [
31 | "atomic-spinner #{assigns.class}",
32 | !assigns.show && "hidden",
33 | size_classes
34 | ]
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/atomic_web/components/unauthenticated.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Components.Unauthenticated do
2 | @moduledoc """
3 | A component for displaying an unauthenticated state.
4 | """
5 | use AtomicWeb, :component
6 |
7 | attr :id, :string, default: "unauthenticated-state", required: false
8 | attr :url, :string, default: "users/log_in", required: false
9 |
10 | def unauthenticated_state(assigns) do
11 | ~H"""
12 |
13 | <.icon name="hero-user-circle" class="mx-auto h-12 w-12 text-zinc-400" />
14 |
{gettext("You are not authenticated")}
15 |
{gettext("Please log in to view this content.")}
16 |
17 | <.button patch={@url} icon="hero-arrow-right-end-on-rectangle-solid" icon_position={:right}>
18 | {gettext("Log In")}
19 |
20 |
21 |
22 | """
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/atomic_web/controllers/sitemap_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Controllers.SitemapController do
2 | use AtomicWeb, :controller
3 |
4 | @host System.get_env("PHX_HOST") || "localhost:4000"
5 |
6 | def index(conn, _params) do
7 | paths = [
8 | "/",
9 | "/activities",
10 | "/organizations",
11 | "/announcements",
12 | "/tos",
13 | "/privacy",
14 | "/cookies"
15 | ]
16 |
17 | urls = Enum.map(paths, &build_path/1)
18 |
19 | xml = """
20 |
21 |
22 | #{Enum.map_join(urls, "\n", fn url -> "#{url} " end)}
23 |
24 | """
25 |
26 | conn
27 | |> put_resp_content_type("application/xml")
28 | |> send_resp(200, xml)
29 | end
30 |
31 | defp build_path(path), do: "https://#{@host}#{path}"
32 | end
33 |
--------------------------------------------------------------------------------
/lib/atomic_web/controllers/user_change_password_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserChangePasswordController do
2 | use AtomicWeb, :controller
3 |
4 | alias Atomic.Accounts
5 | alias AtomicWeb.UserAuth
6 |
7 | plug :assign_password_changeset
8 |
9 | def edit(conn, _params) do
10 | render(conn, "edit.html", error_message: nil)
11 | end
12 |
13 | def update(conn, %{"user" => user_params}) do
14 | user = conn.assigns.current_user
15 |
16 | case Accounts.update_user_password(user, user_params["current_password"], user_params) do
17 | {:ok, user} ->
18 | conn
19 | |> put_flash(:info, "Password updated successfully.")
20 | |> put_session(:user_return_to, ~p"/users/change_password")
21 | |> UserAuth.log_in_user(user)
22 |
23 | {:error, changeset} ->
24 | render(conn, "edit.html", changeset: changeset, error_message: "Password didn't change.")
25 | end
26 | end
27 |
28 | defp assign_password_changeset(conn, _opts) do
29 | user = conn.assigns.current_user
30 |
31 | conn
32 | |> assign(:changeset, Accounts.change_user_password(user))
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/atomic_web/controllers/user_confirmation_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserConfirmationController do
2 | use AtomicWeb, :controller
3 |
4 | alias Atomic.Accounts
5 |
6 | def new(conn, _params) do
7 | render(conn, "new.html", error_message: nil)
8 | end
9 |
10 | def create(conn, %{"user" => %{"email" => email}}) do
11 | if user = Accounts.get_user_by_email(email) do
12 | Accounts.deliver_user_confirmation_instructions(
13 | user,
14 | &url(~p"/users/confirm/#{&1}")
15 | )
16 | end
17 |
18 | conn
19 | |> put_flash(
20 | :info,
21 | "If your email is in our system and it has not been confirmed yet, " <>
22 | "you will receive an email with instructions shortly."
23 | )
24 | |> redirect(to: "/")
25 | end
26 |
27 | def edit(conn, %{"token" => token}) do
28 | update(conn, token)
29 | end
30 |
31 | # Do not log in the user after confirmation to avoid a
32 | # leaked token giving the user access to the account.
33 | def update(conn, token) do
34 | case Accounts.confirm_user(token) do
35 | {:ok, _} ->
36 | conn
37 | |> put_flash(
38 | :info,
39 | "User confirmed successfully. Please log in to continue account setup."
40 | )
41 | |> redirect(to: "/users/log_in")
42 |
43 | :error ->
44 | # If there is a current user and the account was already confirmed,
45 | # then odds are that the confirmation link was already visited, either
46 | # by some automation or by the user themselves, so we redirect without
47 | # a warning message.
48 | case conn.assigns do
49 | %{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
50 | redirect(conn, to: "/")
51 |
52 | %{} ->
53 | conn
54 | |> put_flash(:error, "User confirmation link is invalid or it has expired.")
55 | |> redirect(to: "/users/log_in")
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/atomic_web/controllers/user_registration_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserRegistrationController do
2 | use AtomicWeb, :controller
3 |
4 | alias Atomic.Accounts
5 |
6 | def create(conn, %{"user" => user_params}) do
7 | if user_params["password"] == user_params["confirm_password"] do
8 | case Accounts.register_user(user_params) do
9 | {:ok, user} ->
10 | {:ok, _} =
11 | Accounts.deliver_user_confirmation_instructions(
12 | user,
13 | &url(~p"/users/confirm/#{&1}")
14 | )
15 |
16 | conn
17 | |> put_flash(
18 | :info,
19 | "Registered successfully. Check your email inbox before continuing."
20 | )
21 | |> redirect(to: ~p"/users/register")
22 |
23 | {:error, %Ecto.Changeset{} = _changeset} ->
24 | conn
25 | |> put_flash(:error, "Unable to register. This email may already be registered.")
26 | |> redirect(to: ~p"/users/register")
27 | end
28 | else
29 | conn
30 | |> put_flash(:error, "Passwords don't match.")
31 | |> redirect(to: ~p"/users/register")
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/atomic_web/controllers/user_reset_password_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserResetPasswordController do
2 | use AtomicWeb, :controller
3 |
4 | alias Atomic.Accounts
5 |
6 | plug :get_user_by_reset_password_token when action in [:edit, :update]
7 |
8 | def new(conn, _params) do
9 | render(conn, "new.html", error_message: nil)
10 | end
11 |
12 | def create(conn, %{"user" => %{"input" => input}}) do
13 | user = Accounts.get_user_by_email(input) || Accounts.get_user_by_slug(input)
14 |
15 | if user do
16 | Accounts.deliver_user_reset_password_instructions(
17 | user,
18 | &url(~p"/users/reset_password/#{&1}")
19 | )
20 | end
21 |
22 | conn
23 | |> put_flash(
24 | :info,
25 | "If your email or username is in our system, you will receive instructions to reset your password shortly."
26 | )
27 | |> redirect(to: ~p"/users/log_in")
28 | end
29 |
30 | def edit(conn, _params) do
31 | render(conn, "edit.html",
32 | changeset: Accounts.change_user_password(conn.assigns.user),
33 | error_message: nil
34 | )
35 | end
36 |
37 | # Do not log in the user after reset password to avoid a
38 | # leaked token giving the user access to the account.
39 | def update(conn, %{"user" => user_params}) do
40 | case Accounts.reset_user_password(conn.assigns.user, user_params) do
41 | {:ok, _} ->
42 | conn
43 | |> put_flash(:info, "Password changed successfully.")
44 | |> redirect(to: ~p"/users/log_in")
45 |
46 | {:error, changeset} ->
47 | render(conn, "edit.html", changeset: changeset, error_message: nil)
48 | end
49 | end
50 |
51 | defp get_user_by_reset_password_token(conn, _opts) do
52 | %{"token" => token} = conn.params
53 |
54 | if user = Accounts.get_user_by_reset_password_token(token) do
55 | conn |> assign(:user, user) |> assign(:token, token)
56 | else
57 | conn
58 | |> put_flash(:error, "Reset password link is invalid or it has expired.")
59 | |> redirect(to: "/404")
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/atomic_web/controllers/user_session_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserSessionController do
2 | use AtomicWeb, :controller
3 |
4 | alias Atomic.Accounts
5 | alias AtomicWeb.UserAuth
6 |
7 | def new(conn, %{"user" => user_params}) do
8 | case Accounts.register_user(user_params) do
9 | {:ok, %{user: user, attendee: _}} ->
10 | {:ok, _} =
11 | Accounts.deliver_user_confirmation_instructions(
12 | user,
13 | &url(~p"/users/confirm/#{&1}")
14 | )
15 |
16 | conn
17 | |> UserAuth.log_in_user(user, user_params)
18 | |> put_flash(:success, "Registered successfully")
19 | |> redirect(to: ~p"/users/setup")
20 |
21 | {:error, _, %Ecto.Changeset{} = _changeset, _} ->
22 | conn
23 | |> put_flash(:error, "Unable to register. This email may already be registered.")
24 | |> redirect(to: ~p"/users/register")
25 | end
26 | end
27 |
28 | def create(conn, %{"user" => user_params}) do
29 | %{"email" => email, "password" => password} = user_params
30 | user = Accounts.get_user_by_email_and_password(email, password)
31 |
32 | if user do
33 | if is_nil(user.confirmed_at) do
34 | conn
35 | |> put_flash(:error, "You need to confirm your email address.")
36 | |> redirect(to: ~p"/users/log_in")
37 | else
38 | UserAuth.log_in_user(conn, user, user_params)
39 | end
40 | else
41 | # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
42 | conn
43 | |> put_flash(:error, "Invalid email or password.")
44 | |> redirect(to: ~p"/users/log_in")
45 | end
46 | end
47 |
48 | def delete(conn, _params) do
49 | conn
50 | |> put_flash(:info, "Logged out successfully.")
51 | |> UserAuth.log_out_user()
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/atomic_web/controllers/user_setup_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserSetupController do
2 | use AtomicWeb, :controller
3 |
4 | alias Atomic.Accounts
5 |
6 | @forbidden_characters "!#$%&'*+-/=?^`{|}~"
7 |
8 | def edit(conn, _params) do
9 | user = conn.assigns.current_user
10 | courses = Accounts.list_courses()
11 |
12 | recommended_slug =
13 | String.replace(
14 | extract_email_address_local_part(user.email),
15 | ~r/[#{@forbidden_characters}]+/,
16 | ""
17 | )
18 |
19 | changeset = Accounts.change_user_setup(Map.put(user, :slug, recommended_slug))
20 |
21 | render(conn, "edit.html", changeset: changeset, courses: courses)
22 | end
23 |
24 | def finish(conn, %{"user" => user_params}) do
25 | user = conn.assigns.current_user
26 |
27 | case Accounts.finish_user_setup(user, user_params) do
28 | {:ok, _user} ->
29 | conn
30 | |> put_flash(:info, "Account setup complete.")
31 | |> redirect(to: "/organizations")
32 |
33 | {:error, %Ecto.Changeset{} = changeset} ->
34 | render(conn, "edit.html", changeset: changeset, courses: Accounts.list_courses())
35 | end
36 | end
37 |
38 | defp extract_email_address_local_part(email) do
39 | segments =
40 | email
41 | |> String.split("@")
42 |
43 | List.first(segments)
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/atomic_web/emails/activity_emails.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.ActivityEmails do
2 | @moduledoc """
3 | A module to build activity related emails.
4 | """
5 | use Phoenix.Swoosh, view: AtomicWeb.EmailView
6 |
7 | def activity_certificate_email(enrollment, activity, organizations, certificate, to: email) do
8 | base_email(to: email)
9 | |> subject("[Atomic] Certificado de Participação em \"#{activity.title}\"")
10 | |> assign(:enrollment, enrollment)
11 | |> assign(:activity, activity)
12 | |> assign(:organizations, organizations)
13 | |> attachment(certificate)
14 | |> render_body("activity_certificate.html")
15 | end
16 |
17 | defp base_email(to: email) do
18 | new()
19 | |> from({"Atomic", "noreply@atomic.cesium.pt"})
20 | |> to(email)
21 | |> reply_to("caos@cesium.di.uminho.pt")
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/atomic_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :atomic
3 |
4 | # The session will be stored in the cookie and signed,
5 | # this means its contents can be read but not tampered with.
6 | # Set :encryption_salt if you would also like to encrypt it.
7 | @session_options [
8 | store: :cookie,
9 | key: "_atomic_key",
10 | signing_salt: "2VgE/CCH"
11 | ]
12 |
13 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
14 |
15 | # Serve at "/" the static files from "priv/static" directory.
16 | #
17 | # You should set gzip to true if you are running phx.digest
18 | # when deploying your static files in production.
19 | plug Plug.Static,
20 | at: "/",
21 | from: :atomic,
22 | gzip: false,
23 | only: AtomicWeb.static_paths()
24 |
25 | plug(Plug.Static,
26 | at: "/uploads",
27 | from: Path.expand("./priv/uploads"),
28 | gzip: false
29 | )
30 |
31 | # Code reloading can be explicitly enabled under the
32 | # :code_reloader configuration of your endpoint.
33 | if code_reloading? do
34 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
35 | plug Phoenix.LiveReloader
36 | plug Phoenix.CodeReloader
37 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :atomic
38 | end
39 |
40 | plug Phoenix.LiveDashboard.RequestLogger,
41 | param_key: "request_logger",
42 | cookie_key: "request_logger"
43 |
44 | plug Plug.RequestId
45 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
46 |
47 | plug Plug.Parsers,
48 | parsers: [:urlencoded, :multipart, :json],
49 | pass: ["*/*"],
50 | json_decoder: Phoenix.json_library()
51 |
52 | plug Plug.MethodOverride
53 | plug Plug.Head
54 | plug Plug.Session, @session_options
55 | plug AtomicWeb.Router
56 | end
57 |
--------------------------------------------------------------------------------
/lib/atomic_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.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 AtomicWeb.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.Backend, otp_app: :atomic
24 | end
25 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/activity_live/edit.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.ActivityLive.Edit do
2 | @moduledoc false
3 | use AtomicWeb, :live_view
4 |
5 | alias Atomic.Activities
6 |
7 | import AtomicWeb.LiveHelpers
8 |
9 | @impl true
10 | def mount(_params, _session, socket) do
11 | {:ok, socket}
12 | end
13 |
14 | @impl true
15 | def handle_params(%{"id" => id}, _, socket) do
16 | activity = Activities.get_activity!(id, [:organization])
17 |
18 | {:noreply,
19 | socket
20 | |> assign(:page_title, "Edit Activity")
21 | |> assign_page_metadata(:edit_activity)
22 | |> assign(:current_page, :activities)
23 | |> assign(:current_organization, activity.organization)
24 | |> assign(:activity, activity)}
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/activity_live/edit.html.heex:
--------------------------------------------------------------------------------
1 |
2 | <.live_component module={AtomicWeb.ActivityLive.FormComponent} id={@activity.id} title={@page_title} action={@live_action} activity={@activity} current_organization={@current_organization} return_to={~p"/activities/#{@activity}"} />
3 |
4 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/activity_live/new.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.ActivityLive.New do
2 | @moduledoc false
3 | use AtomicWeb, :live_view
4 |
5 | alias Atomic.Activities.Activity
6 |
7 | import AtomicWeb.LiveHelpers
8 |
9 | @impl true
10 | def mount(_params, _session, socket) do
11 | {:ok, assign(socket, activity: %Activity{})}
12 | end
13 |
14 | @impl true
15 | def handle_params(%{"organization_id" => organization_id}, _, socket) do
16 | {:noreply,
17 | socket
18 | |> assign(:page_title, "New Activity")
19 | |> assign_page_metadata(:new_activity)
20 | |> assign(:current_page, :activities)
21 | |> assign(:organization_id, organization_id)}
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/activity_live/new.html.heex:
--------------------------------------------------------------------------------
1 |
2 | <.live_component module={AtomicWeb.ActivityLive.FormComponent} id={:new} title={@page_title} action={@live_action} activity={@activity} current_organization={@current_organization} return_to={~p"/activities"} />
3 |
4 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/components/announcement_card.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.AnnouncementLive.Components.AnnouncementCard do
2 | @moduledoc false
3 |
4 | import AtomicWeb.Components.Avatar
5 |
6 | use AtomicWeb, :component
7 |
8 | def announcement_card(assigns) do
9 | ~H"""
10 |
11 | <.link navigate={~p"/organizations/#{@organization}/announcements/#{@announcement}"} class="block">
12 |
13 |
14 | <.avatar name={@announcement.organization.name} color={:light_zinc} class="!h-10 !w-10" size={:xs} type={:organization} src={Uploaders.Logo.url({@announcement.organization.logo, @announcement.organization}, :original)} />
15 |
16 |
17 |
{@announcement.organization.name}
18 |
19 | Published on
20 | {relative_datetime(@announcement.inserted_at)}
21 |
22 |
23 |
24 |
25 |
26 | {@announcement.title}
27 |
28 |
29 | {maybe_slice_string(@announcement.description, 300)}
30 |
31 |
32 | <%= if @announcement.image do %>
33 |
34 |
35 |
36 | <% end %>
37 |
38 |
39 | """
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/edit.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.AnnouncementLive.Edit do
2 | @moduledoc false
3 | use AtomicWeb, :live_view
4 |
5 | alias Atomic.Organizations
6 |
7 | import AtomicWeb.LiveHelpers
8 |
9 | @impl true
10 | def mount(_params, _session, socket) do
11 | {:ok, socket}
12 | end
13 |
14 | @impl true
15 | def handle_event("delete", _params, socket) do
16 | Organizations.delete_announcement(socket.assigns.announcement)
17 |
18 | {:noreply,
19 | socket
20 | |> put_flash(:info, gettext("Announcement deleted successfully"))
21 | |> push_navigate(
22 | to: ~p"/organizations/#{socket.assigns.current_organization.id}/announcements"
23 | )}
24 | end
25 |
26 | @impl true
27 | def handle_params(%{"organization_id" => organization_id, "id" => id}, _, socket) do
28 | announcement = Organizations.get_announcement!(id)
29 | organization = Organizations.get_organization!(organization_id)
30 |
31 | {:noreply,
32 | socket
33 | |> assign(:page_title, gettext("Edit Announcement"))
34 | |> assign_page_metadata(:edit_announcement)
35 | |> assign(:current_page, :activities)
36 | |> assign(:announcement, announcement)
37 | |> assign(:current_organization, organization)}
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/edit.html.heex:
--------------------------------------------------------------------------------
1 | <.page title={gettext("Edit Announcement")}>
2 | <:actions>
3 | <.button size={:md} icon="hero-trash" color={:white} type="delete" phx-click="delete">
4 | {gettext("Delete")}
5 |
6 |
7 |
8 | <.live_component module={AtomicWeb.AnnouncementLive.FormComponent} id={@announcement.id} organization={@current_organization} title={@page_title} action={@live_action} announcement={@announcement} return_to={~p"/organizations/#{@current_organization}/announcements"} />
9 |
10 |
11 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/form_component.html.heex:
--------------------------------------------------------------------------------
1 |
2 | <.form :let={f} for={@changeset} id="announcement-form" phx-target={@myself} phx-change="validate" phx-submit="save" class="space-y-6">
3 |
4 |
5 |
6 | <.field field={f[:title]} type="text" placeholder="Title" required class="w-full" />
7 |
8 | <.field field={f[:description]} type="textarea" placeholder="Description" required class="h-44 w-full resize-none overflow-auto xl:h-64" />
9 |
10 |
11 | <.live_component module={ImageUploader} id="uploader" uploads={@uploads} target={@myself} class="object-cover" />
12 |
13 |
14 | <.button size={:md} color={:white} icon="hero-cube" type="submit">{gettext("Save Changes")}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/index.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.AnnouncementLive.Index do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.Components.{Button, Empty, Pagination}
5 | import AtomicWeb.AnnouncementLive.Components.AnnouncementCard
6 | import AtomicWeb.LiveHelpers
7 |
8 | alias Atomic.Accounts
9 | alias Atomic.Organizations
10 |
11 | @impl true
12 | def mount(_params, _session, socket) do
13 | {:ok, socket}
14 | end
15 |
16 | @impl true
17 | def handle_params(%{"organization_id" => organization_id} = params, _, socket) do
18 | organization = Organizations.get_organization!(organization_id)
19 |
20 | {:noreply,
21 | socket
22 | |> assign(:page_title, gettext("Announcements"))
23 | |> assign_page_metadata(:announcements)
24 | |> assign(:current_page, :announcements)
25 | |> assign(:organization, organization)
26 | |> assign(:params, params)
27 | |> assign(:has_permissions?, has_permissions?(socket))
28 | |> assign(list_announcements_by_organization(socket, params, organization_id))
29 | |> then(fn complete_socket ->
30 | assign(complete_socket, :empty?, Enum.empty?(complete_socket.assigns.announcements))
31 | end)}
32 | end
33 |
34 | defp list_announcements_by_organization(_socket, params, organization_id) do
35 | case Organizations.list_announcements_by_organization_id(organization_id, params,
36 | preloads: [:organization]
37 | ) do
38 | {:ok, {announcements, meta}} ->
39 | %{announcements: announcements, meta: meta}
40 |
41 | {:error, flop} ->
42 | %{announcements: [], meta: flop}
43 | end
44 | end
45 |
46 | defp has_permissions?(socket) when not socket.assigns.is_authenticated?, do: false
47 |
48 | defp has_permissions?(socket) do
49 | has_current_organization?(socket) and
50 | (Accounts.has_permissions_inside_organization?(
51 | socket.assigns.current_user.id,
52 | socket.assigns.current_organization.id
53 | ) or Accounts.has_master_permissions?(socket.assigns.current_user.id))
54 | end
55 |
56 | defp has_current_organization?(socket) do
57 | is_map_key(socket.assigns, :current_organization) and
58 | not is_nil(socket.assigns.current_organization)
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/index.html.heex:
--------------------------------------------------------------------------------
1 | <.page title="Announcements" bottom_border={true}>
2 | <:actions>
3 | <%= if not @empty? and @has_permissions? do %>
4 | <.button navigate={~p"/organizations/#{@current_organization}/announcements/new"} icon="hero-plus">
5 | {gettext("New Announcement")}
6 |
7 | <% end %>
8 |
9 |
10 | <%= if @empty? and @has_permissions? do %>
11 |
12 | <.empty_state url={~p"/organizations/#{@organization}/announcements/new"} placeholder="announcement" />
13 |
14 | <% else %>
15 |
16 |
17 | <%= for announcement <- @announcements do %>
18 |
19 | <.announcement_card announcement={announcement} organization={@organization} />
20 |
21 | <% end %>
22 |
23 | <.pagination items={@announcements} meta={@meta} params={@params} class="mt-2 flex w-full items-center justify-between" />
24 |
25 | <% end %>
26 |
27 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/new.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.AnnouncementLive.New do
2 | @moduledoc false
3 | use AtomicWeb, :live_view
4 |
5 | alias Atomic.Organizations.Announcement
6 |
7 | import AtomicWeb.LiveHelpers
8 |
9 | @impl true
10 | def mount(_params, _session, socket) do
11 | {:ok, socket}
12 | end
13 |
14 | @impl true
15 | def handle_params(%{"organization_id" => organization_id}, _, socket) do
16 | {:noreply,
17 | socket
18 | |> assign(:page_title, gettext("New Announcement"))
19 | |> assign_page_metadata(:new_announcement)
20 | |> assign(:current_page, :announcements)
21 | |> assign(:announcement, %Announcement{organization_id: organization_id})}
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/new.html.heex:
--------------------------------------------------------------------------------
1 | <.page title={gettext("New Announcement")}>
2 |
3 | <.live_component module={AtomicWeb.AnnouncementLive.FormComponent} id={:new} organization={@current_organization} title={@page_title} action={@live_action} announcement={@announcement} return_to={~p"/organizations/#{@current_organization}/announcements"} />
4 |
5 |
6 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/show.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.AnnouncementLive.Show do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.Components.Avatar
5 | import AtomicWeb.LiveHelpers
6 |
7 | alias Atomic.Accounts
8 | alias Atomic.Organizations
9 |
10 | @impl true
11 | def mount(_params, _session, socket) do
12 | {:ok, socket}
13 | end
14 |
15 | @impl true
16 | def handle_params(%{"id" => id} = _params, _, socket) do
17 | announcement = Organizations.get_announcement!(id, preloads: [:organization])
18 |
19 | {:noreply,
20 | socket
21 | |> assign(:page_title, announcement.title)
22 | |> assign_page_metadata(:announcement)
23 | |> assign(:current_page, :announcements)
24 | |> assign(:announcement, announcement)
25 | |> assign(:has_permissions?, has_permissions?(socket |> assign(:announcement, announcement)))}
26 | end
27 |
28 | defp has_permissions?(socket) when not socket.assigns.is_authenticated?, do: false
29 |
30 | defp has_permissions?(socket)
31 | when not is_map_key(socket.assigns, :current_organization) or
32 | is_nil(socket.assigns.current_organization) do
33 | Accounts.has_master_permissions?(socket.assigns.current_user.id)
34 | end
35 |
36 | defp has_permissions?(socket) do
37 | Accounts.has_master_permissions?(socket.assigns.current_user.id) ||
38 | Accounts.has_permissions_inside_organization?(
39 | socket.assigns.current_user.id,
40 | socket.assigns.announcement.organization.id
41 | )
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/announcement_live/show.html.heex:
--------------------------------------------------------------------------------
1 | <.page title="Announcements" bottom_border={true}>
2 | <:actions>
3 | <%= if @has_permissions? do %>
4 | <.button navigate={~p"/organizations/#{@announcement.organization}/announcements/#{@announcement}/edit"} icon="hero-pencil-solid">{gettext("Edit Announcement")}
5 | <% end %>
6 |
7 |
8 |
9 |
10 |
11 | <.avatar name={@announcement.organization.name} color={:light_zinc} size={:md} type={:organization} src={Uploaders.Logo.url({@announcement.organization.logo, @announcement.organization}, :original)} />
12 |
13 |
14 | <.link navigate={~p"/organizations/#{@announcement.organization.id}"} class="hover:underline focus:outline-none">
15 |
{@announcement.organization.name}
16 |
17 |
18 | Published on
19 | {relative_datetime(@announcement.inserted_at)}
20 |
21 |
22 |
23 |
24 |
25 | {@announcement.title}
26 |
27 |
28 |
29 | <%= Enum.map(String.split(@announcement.description, "\n"), fn phrase -> %>
30 | {phrase}
31 | <% end) %>
32 |
33 |
34 |
35 | <%= if @announcement.image do %>
36 |
37 |
38 |
39 | <% end %>
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/department_live/components/department_card.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.DepartmentLive.Components.DepartmentCard do
2 | @moduledoc false
3 | use AtomicWeb, :component
4 |
5 | import AtomicWeb.Components.{Avatar, Badge, Gradient}
6 |
7 | attr :department, :map, required: true, doc: "The department to display."
8 | attr :collaborators, :list, required: true, doc: "The list of collaborators in the department."
9 |
10 | def department_card(assigns) do
11 | ~H"""
12 |
13 |
14 | <%= if @department.banner do %>
15 |
16 | <% else %>
17 | <.gradient seed={@department.id} class="rounded-t-lg" />
18 | <% end %>
19 |
20 |
21 |
22 |
{@department.name}
23 | <.badge :if={@department.archived} variant={:outline} color={:warning} size={:md} class="bg-yellow-300/5 select-none rounded-xl border-yellow-400 py-1 font-normal text-yellow-400 sm:ml-auto sm:py-0">
24 |
{gettext("Archived")}
25 |
26 |
27 | <.avatar_group
28 | size={:xs}
29 | color={:light_zinc}
30 | spacing={-2}
31 | class="min-h-8 mt-4 mb-2"
32 | items={
33 | @collaborators
34 | |> Enum.take(4)
35 | |> Enum.map(fn person ->
36 | %{
37 | name: person.user.name
38 | }
39 | end)
40 | |> then(fn avatars ->
41 | if length(@collaborators) > 4 do
42 | Enum.concat(avatars, [%{name: "+#{length(@collaborators) - 4}", auto_generate_initials: false}])
43 | else
44 | avatars
45 | end
46 | end)
47 | }
48 | />
49 |
50 |
51 | """
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/department_live/index.html.heex:
--------------------------------------------------------------------------------
1 | <.page title="Departments">
2 | <:actions>
3 | <%= if not @empty? and @has_permissions? do %>
4 | <.button patch={~p"/organizations/#{@current_organization}/departments/new"} icon="hero-plus">
5 | {gettext("New")}
6 |
7 | <% end %>
8 |
9 |
10 | <%= if @empty? and @has_permissions? do %>
11 |
12 | <.empty_state url={~p"/organizations/#{@current_organization}/departments/new"} placeholder="department" />
13 |
14 | <% else %>
15 |
16 | <%= for {department, collaborators} <- @departments do %>
17 | <.link navigate={~p"/organizations/#{@current_organization}/departments/#{department}"}>
18 | <.department_card department={department} collaborators={collaborators} />
19 |
20 | <% end %>
21 |
22 | <% end %>
23 |
24 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/home_live/components/follow_suggestions/follow_suggestions.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.HomeLive.Components.FollowSuggestions do
2 | @moduledoc false
3 | use AtomicWeb, :component
4 |
5 | alias __MODULE__.Suggestion
6 |
7 | attr :current_user, :map,
8 | required: true,
9 | doc: "The current user logged in."
10 |
11 | attr :organizations, :list,
12 | required: true,
13 | doc: "Organizations displayed as follow suggestions."
14 |
15 | def follow_suggestions(assigns) do
16 | ~H"""
17 |
18 |
19 | {title(@current_user)}
20 |
21 |
22 |
23 | <%= for organization <- @organizations do %>
24 | <.live_component id={organization.id} module={Suggestion} organization={organization} current_user={@current_user} />
25 | <% end %>
26 |
27 |
28 |
29 | <.button patch={~p"/organizations"} color={:white} size={:md} full_width>
30 | {gettext("View all")}
31 |
32 |
33 |
34 | """
35 | end
36 |
37 | defp title(current_user) when is_nil(current_user), do: gettext("Top organizations")
38 |
39 | defp title(_current_user), do: gettext("Organizations you may like")
40 | end
41 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/hooks.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Hooks do
2 | @moduledoc """
3 | Ensures common `assigns` are applied to all LiveViews attaching this hook.
4 | """
5 | import Phoenix.LiveView
6 | import Phoenix.Component
7 |
8 | alias Atomic.Accounts
9 |
10 | def on_mount(:current_user_state, _params, session, socket) do
11 | current_user = maybe_get_current_user(session)
12 | socket = socket |> assign(:timezone, get_timezone(socket))
13 |
14 | {:cont,
15 | socket
16 | |> assign(:current_user, current_user)
17 | |> assign(:current_organization, maybe_get_current_organization(session))
18 | |> assign(:is_authenticated?, !is_nil(current_user))}
19 | end
20 |
21 | defp maybe_get_current_user(session) do
22 | case session["user_token"] do
23 | nil ->
24 | nil
25 |
26 | user_token ->
27 | Accounts.get_user_by_session_token(user_token)
28 | end
29 | end
30 |
31 | defp maybe_get_current_organization(session) do
32 | case maybe_get_current_user(session) do
33 | nil ->
34 | nil
35 |
36 | current_user ->
37 | current_user.current_organization
38 | end
39 | end
40 |
41 | defp get_timezone(socket) do
42 | timezone = Application.get_env(:atomic, :timezone)
43 | get_connect_params(socket)["timezone"] || timezone
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/legal_terms_live/components/black_bar.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.LegalTermsLive.Components.BlackBar do
2 | @moduledoc """
3 | Component for Legal Pages Black Bar.
4 | """
5 | use Phoenix.Component
6 | use AtomicWeb, :component
7 |
8 | def black_bar(assigns) do
9 | ~H"""
10 |
11 |
12 | {gettext("Lorem ipsum dolor sit amet.")}
13 |
14 |
15 | """
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/legal_terms_live/components/header.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.LegalTermsLive.Components.Header do
2 | @moduledoc """
3 | Component for Legal Pages Header.
4 | """
5 | use Phoenix.Component
6 | use AtomicWeb, :component
7 |
8 | @pages [
9 | {"Terms of Service", "tos"},
10 | {"Privacy Policy", "privacy"},
11 | {"Cookie Policy", "cookies"}
12 | ]
13 |
14 | defp link_pages(current_page) do
15 | Enum.map(@pages, fn {title, path} ->
16 | if title == current_page do
17 | {:current, title, path}
18 | else
19 | {:link, title, path}
20 | end
21 | end)
22 | end
23 |
24 | def header(assigns) do
25 | ~H"""
26 |
27 |
28 |
29 |
30 | <.link navigate={~p"/"}>
31 |
32 |
33 |
34 |
35 | <%= for {type, title, path} <- link_pages(@page_name) do %>
36 | <%= case type do %>
37 | <% :current -> %>
38 | {title}
39 | <% :link -> %>
40 | <.link class="hover:text-zinc-400" navigate={"/" <> path}>{title}
41 | <% end %>
42 | <% end %>
43 |
44 |
45 |
46 |
47 | <.button class="atomic-button atomic-button--white atomic-button--md hidden sm:block" patch={~p"/"}>{gettext("Back Home")}
48 | <.button class="atomic-button atomic-button--md hero-home block text-zinc-400 sm:hidden" patch={~p"/"}>
49 |
50 |
51 | """
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/legal_terms_live/components/main_title.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.LegalTermsLive.Components.MainTitle do
2 | @moduledoc """
3 | Component for Legal Pages Main Title.
4 | """
5 | use Phoenix.Component
6 | use AtomicWeb, :component
7 |
8 | def main_title(assigns) do
9 | ~H"""
10 |
13 | """
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/legal_terms_live/cookies_live/show.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.CookiesLive.Show do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.LegalTermsLive.Components.{Header, MainTitle, BlackBar}
5 | import AtomicWeb.LiveHelpers
6 |
7 | @impl true
8 | def mount(_params, _session, socket) do
9 | {:ok, socket, layout: false}
10 | end
11 |
12 | @impl true
13 | def handle_params(_params, _, socket) do
14 | {:noreply,
15 | socket
16 | |> assign(:page_title, gettext("Cookies"))
17 | |> assign_page_metadata(:cookies)
18 | |> assign(:current_page, :cookies)}
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/legal_terms_live/privacy_live/show.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.PrivacyLive.Show do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.LegalTermsLive.Components.{Header, MainTitle, BlackBar}
5 | import AtomicWeb.LiveHelpers
6 |
7 | @impl true
8 | def mount(_params, _session, socket) do
9 | {:ok, socket, layout: false}
10 | end
11 |
12 | @impl true
13 | def handle_params(_params, _, socket) do
14 | {:noreply,
15 | socket
16 | |> assign(:page_title, gettext("Privacy Policy"))
17 | |> assign_page_metadata(:privacy)
18 | |> assign(:current_page, :privacy)}
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/legal_terms_live/tos_live/show.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.TermsLive.Show do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.LegalTermsLive.Components.{Header, MainTitle, BlackBar}
5 | import AtomicWeb.LiveHelpers
6 |
7 | @impl true
8 | def mount(_params, _session, socket) do
9 | {:ok, socket, layout: false}
10 | end
11 |
12 | @impl true
13 | def handle_params(_params, _, socket) do
14 | {:noreply,
15 | socket
16 | |> assign(:page_title, gettext("Terms of Service"))
17 | |> assign_page_metadata(:terms)
18 | |> assign(:current_page, :terms)}
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/organization_live/edit.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.OrganizationLive.Edit do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.LiveHelpers
5 |
6 | alias Atomic.Organizations
7 |
8 | @impl true
9 | def mount(_params, _session, socket) do
10 | {:ok, socket}
11 | end
12 |
13 | @impl true
14 | def handle_params(%{"organization_id" => organization_id}, _, socket) do
15 | organization = Organizations.get_organization!(organization_id)
16 |
17 | {:noreply,
18 | socket
19 | |> assign(:page_title, organization.name)
20 | |> assign_page_metadata(:organization)
21 | |> assign(:organization, organization)
22 | |> assign(:current_page, :organizations)}
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/organization_live/edit.html.heex:
--------------------------------------------------------------------------------
1 | <.live_component module={AtomicWeb.OrganizationLive.FormComponent} id={@organization} title={@page_title} action={@live_action} organization={@organization} return_to={~p"/organizations/#{@organization}"} />
2 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/organization_live/form_component.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.OrganizationLive.FormComponent do
2 | use AtomicWeb, :live_component
3 |
4 | alias Atomic.Organizations
5 |
6 | @impl true
7 | def mount(socket) do
8 | {:ok, socket}
9 | end
10 |
11 | @impl true
12 | def update(%{organization: organization} = assigns, socket) do
13 | changeset = Organizations.change_organization(organization)
14 |
15 | {:ok,
16 | socket
17 | |> assign(assigns)
18 | |> assign(:changeset, changeset)}
19 | end
20 |
21 | @impl true
22 | def handle_event("validate", %{"organization" => organization_params}, socket) do
23 | changeset =
24 | socket.assigns.organization
25 | |> Organizations.change_organization(organization_params)
26 | |> Map.put(:action, :validate)
27 |
28 | {:noreply, assign(socket, :changeset, changeset)}
29 | end
30 |
31 | def handle_event("save", %{"organization" => organization_params}, socket) do
32 | save_organization(socket, socket.assigns.action, organization_params)
33 | end
34 |
35 | defp save_organization(socket, :edit, organization_params) do
36 | case Organizations.update_organization(socket.assigns.organization, organization_params) do
37 | {:ok, _organization} ->
38 | {:noreply,
39 | socket
40 | |> put_flash(:info, "Organization updated successfully")
41 | |> push_navigate(to: socket.assigns.return_to)}
42 |
43 | {:error, %Ecto.Changeset{} = changeset} ->
44 | {:noreply, assign(socket, :changeset, changeset)}
45 | end
46 | end
47 |
48 | defp save_organization(socket, :new, organization_params) do
49 | case Organizations.create_organization(organization_params) do
50 | {:ok, _organization} ->
51 | {:noreply,
52 | socket
53 | |> put_flash(:info, "Organization created successfully")
54 | |> push_navigate(to: socket.assigns.return_to)}
55 |
56 | {:error, %Ecto.Changeset{} = changeset} ->
57 | {:noreply, assign(socket, changeset: changeset)}
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/organization_live/form_component.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
{@title}
3 |
4 | <.form :let={f} for={@changeset} id="organization-form" phx-target={@myself} phx-change="validate" phx-submit="save">
5 | {label(f, :name)}
6 | {text_input(f, :name)}
7 | {error_tag(f, :name)}
8 |
9 | {label(f, :description)}
10 | {text_input(f, :description)}
11 | {error_tag(f, :description)}
12 |
13 |
14 | {submit("Save", phx_disable_with: "Saving...")}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/organization_live/index.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.OrganizationLive.Index do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.Components.Avatar
5 | import AtomicWeb.Components.Empty
6 | import AtomicWeb.Components.Pagination
7 | import AtomicWeb.Components.Button
8 | import AtomicWeb.LiveHelpers
9 |
10 | alias Atomic.Accounts
11 | alias Atomic.Organizations
12 |
13 | @impl true
14 | def mount(_params, _session, socket) do
15 | {:ok, socket}
16 | end
17 |
18 | @impl true
19 | def handle_params(params, _url, socket) do
20 | organizations_with_flop = list_organizations(params)
21 |
22 | {:noreply,
23 | socket
24 | |> assign(:page_title, gettext("Organizations"))
25 | |> assign_page_metadata(:organizations)
26 | |> assign(:current_page, :organizations)
27 | |> assign(:params, params)
28 | |> assign(organizations_with_flop)
29 | |> assign(:empty?, Enum.empty?(organizations_with_flop.organizations))
30 | |> assign(:has_permissions?, has_permissions?(socket))}
31 | end
32 |
33 | defp list_organizations(params) do
34 | case Organizations.list_organizations(Map.put(params, "page_size", 18)) do
35 | {:ok, {organizations, meta}} ->
36 | %{organizations: organizations, meta: meta}
37 |
38 | {:error, flop} ->
39 | %{organizations: [], meta: flop}
40 | end
41 | end
42 |
43 | defp has_permissions?(socket) when not socket.assigns.is_authenticated?, do: false
44 |
45 | defp has_permissions?(socket) do
46 | Accounts.has_master_permissions?(socket.assigns.current_user.id)
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/organization_live/index.html.heex:
--------------------------------------------------------------------------------
1 | <.page title="Organizations">
2 | <:actions>
3 | <%= if not @empty? and @has_permissions? do %>
4 | <.button navigate={~p"/organizations/new"}>
5 | {gettext("New")}
6 |
7 | <% end %>
8 |
9 |
10 | <%= if @empty? and @has_permissions? do %>
11 |
12 | <.empty_state url={~p"/organizations/new"} placeholder="organization" />
13 |
14 | <% else %>
15 |
16 | <%= for organization <- @organizations do %>
17 | <.link navigate={~p"/organizations/#{organization.id}"}>
18 |
19 | <.avatar name={organization.name} src={Uploaders.Logo.url({organization.logo, organization}, :original)} type={:organization} size={:lg} color={:light_zinc} />
20 |
21 |
22 | {organization.name}
23 |
24 |
25 | {maybe_slice_string(organization.long_name, 35)}
26 |
27 |
28 | {maybe_slice_string(organization.long_name, 85)}
29 |
30 |
31 |
32 |
33 | <% end %>
34 |
35 | <.pagination items={@organizations} meta={@meta} params={@params} class="mt-2 flex w-full items-center justify-between" />
36 | <% end %>
37 |
38 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/organization_live/new.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.OrganizationLive.New do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.LiveHelpers
5 |
6 | alias Atomic.Organizations.Organization
7 |
8 | @impl true
9 | def mount(_params, _session, socket) do
10 | {:ok, socket}
11 | end
12 |
13 | @impl true
14 | def handle_params(_params, _, socket) do
15 | {:noreply,
16 | socket
17 | |> assign(:page_title, gettext("New Organization"))
18 | |> assign_page_metadata(:new_organization)
19 | |> assign(:organization, %Organization{})
20 | |> assign(:current_page, :organization)}
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/organization_live/new.html.heex:
--------------------------------------------------------------------------------
1 | <.live_component module={AtomicWeb.OrganizationLive.FormComponent} current_user={@current_user} organization={@current_organization} id={:new} title={@page_title} action={@live_action} return_to={~p"/organizations"} />
2 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/partner_live/show.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.PartnerLive.Show do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.Components.{Avatar, Socials}
5 | import AtomicWeb.LiveHelpers
6 |
7 | alias Atomic.Accounts
8 | alias Atomic.Organizations
9 | alias Atomic.Partners
10 |
11 | @impl true
12 | def mount(_params, _session, socket) do
13 | {:ok, socket}
14 | end
15 |
16 | @impl true
17 | def handle_params(%{"organization_id" => organization_id, "id" => id}, _, socket) do
18 | organization = Organizations.get_organization!(organization_id)
19 | partner = Partners.get_partner!(id)
20 |
21 | {:noreply,
22 | socket
23 | |> assign(:page_title, partner.name)
24 | |> assign_page_metadata(:partner)
25 | |> assign(:current_page, :partners)
26 | |> assign(:organization, organization)
27 | |> assign(:partner, partner)
28 | |> assign(
29 | :partners,
30 | Partners.list_partners(where: [organization_id: organization_id, archived: false])
31 | )
32 | |> assign(:has_permissions?, has_permissions?(socket, organization_id))}
33 | end
34 |
35 | defp has_permissions?(socket, _organization_id) when not socket.assigns.is_authenticated?,
36 | do: false
37 |
38 | defp has_permissions?(socket, _organization_id)
39 | when not is_map_key(socket.assigns, :current_organization) or
40 | is_nil(socket.assigns.current_organization) do
41 | Accounts.has_master_permissions?(socket.assigns.current_user.id)
42 | end
43 |
44 | defp has_permissions?(socket, organization_id) do
45 | Accounts.has_master_permissions?(socket.assigns.current_user.id) ||
46 | Accounts.has_permissions_inside_organization?(
47 | socket.assigns.current_user.id,
48 | organization_id
49 | )
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/profile_live/edit.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.ProfileLive.Edit do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.LiveHelpers
5 |
6 | alias Atomic.Accounts
7 |
8 | @impl true
9 | def mount(_params, _session, socket) do
10 | {:ok, socket}
11 | end
12 |
13 | @impl true
14 | def handle_params(%{"slug" => user_slug}, _, socket) do
15 | user = Accounts.get_user_by_slug(user_slug)
16 |
17 | if socket.assigns.current_user && socket.assigns.current_user.slug == user_slug do
18 | {:noreply,
19 | socket
20 | |> assign(:page_title, user.name)
21 | |> assign_page_metadata(:user_profile)
22 | |> assign(:current_page, :profile)
23 | |> assign(:user, user)}
24 | else
25 | {:noreply, socket |> redirect(to: ~p"/profile/#{user_slug}")}
26 | end
27 | end
28 |
29 | def handle_params(%{"token" => token}, _, socket) do
30 | case Accounts.update_user_email(socket.assigns.current_user, token) do
31 | :ok ->
32 | {:noreply,
33 | socket
34 | |> put_flash(:info, "Email changed successfully.")
35 | |> redirect(to: ~p"/profile/#{socket.assigns.current_user}")}
36 |
37 | :error ->
38 | {:noreply,
39 | socket
40 | |> put_flash(:error, "Email change link is invalid or it has expired.")
41 | |> redirect(to: ~p"/profile/#{socket.assigns.current_user}")}
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/profile_live/edit.html.heex:
--------------------------------------------------------------------------------
1 | <.live_component id={@user.id} module={AtomicWeb.ProfileLive.FormComponent} user={@user} action={@live_action} />
2 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/profile_live/show.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.ProfileLive.Show do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.Components.{Button, Avatar, Gradient, Socials}
5 | import AtomicWeb.Components.ImageUploader
6 | import AtomicWeb.LiveHelpers
7 |
8 | alias Atomic.Accounts
9 | alias Atomic.Organizations
10 |
11 | @extensions_whitelist ~w(.jpg .jpeg .gif .png)
12 |
13 | @impl true
14 | def mount(_params, _session, socket) do
15 | {:ok,
16 | socket
17 | |> allow_upload(:profile_picture,
18 | accept: @extensions_whitelist,
19 | max_entries: 1,
20 | max_file_size: 10_000_000
21 | )
22 | |> allow_upload(:banner,
23 | accept: @extensions_whitelist,
24 | max_entries: 1,
25 | max_file_size: 100_000_000
26 | )}
27 | end
28 |
29 | @impl true
30 | def handle_params(%{"slug" => user_slug}, _, socket) do
31 | user = Accounts.get_user_by_slug(user_slug)
32 |
33 | is_current_user =
34 | Map.has_key?(socket.assigns, :current_user) and socket.assigns.current_user.id == user.id
35 |
36 | organizations = Organizations.list_user_organizations(user.id)
37 |
38 | {:noreply,
39 | socket
40 | |> assign(:page_title, user.name)
41 | |> assign_page_metadata(:user_profile)
42 | |> assign(:current_page, :profile)
43 | |> assign(:user, user)
44 | |> assign(:organizations, organizations)
45 | |> assign(:is_current_user, is_current_user)}
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/scanner_live/index.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.ScannerLive.Index do
2 | @moduledoc false
3 | use AtomicWeb, :live_view
4 |
5 | alias Atomic.Accounts
6 | alias Atomic.Activities
7 |
8 | @impl true
9 | def mount(_params, _session, socket) do
10 | {:ok, socket}
11 | end
12 |
13 | @impl true
14 | def handle_params(_params, _, socket) do
15 | {:noreply,
16 | socket
17 | |> assign(:current_page, :scanner)
18 | |> assign(:title, "Scanner")}
19 | end
20 |
21 | @doc """
22 | Handles the scan event.
23 | """
24 | @impl true
25 | def handle_event("scan", pathname, socket) do
26 | [_, activity_id, user_id | _] = String.split(pathname, "/")
27 | activity = Activities.get_activity!(activity_id)
28 |
29 | if (socket.assigns.current_organization.id == activity.organization_id &&
30 | Accounts.has_permissions_inside_organization?(
31 | socket.assigns.current_user.id,
32 | socket.assigns.current_organization.id
33 | )) || Accounts.has_master_permissions?(socket.assigns.current_user) do
34 | confirm_participation(socket, activity_id, user_id)
35 | else
36 | {:noreply,
37 | socket
38 | |> put_flash(:error, "You are not authorized to this")
39 | |> redirect(to: ~p"/scanner")}
40 | end
41 | end
42 |
43 | defp confirm_participation(socket, session_id, user_id) do
44 | case Activities.update_enrollment(Activities.get_enrollment!(session_id, user_id), %{
45 | present: true
46 | }) do
47 | {:ok, _} ->
48 | {:noreply,
49 | socket
50 | |> put_flash(:success, "Participation confirmed!")
51 | |> assign(:changeset, nil)
52 | |> redirect(to: ~p"/scanner")}
53 |
54 | {:error, changeset} ->
55 | {:noreply,
56 | socket
57 | |> put_flash(:error, "Unable to confirm participation")
58 | |> assign(:changeset, changeset)}
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/scanner_live/index.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 | Scan a QR code
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Unable to access camera. Make sure you allow the use of the camera and that the camera isn't being used elsewhere.
12 |
13 |
Request Permission
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/user_live/edit.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserLive.Edit do
2 | use AtomicWeb, :live_view
3 |
4 | import AtomicWeb.LiveHelpers
5 |
6 | alias Atomic.Accounts
7 |
8 | @impl true
9 | def mount(_params, _session, socket) do
10 | {:ok, assign(socket, :current_user, socket.assigns.current_user)}
11 | end
12 |
13 | @impl true
14 | def handle_params(_, _, socket) do
15 | {:noreply,
16 | socket
17 | |> assign(:page_title, "Edit Account")
18 | |> assign_page_metadata(:edit_account)
19 | |> assign(:user, socket.assigns.current_user)
20 | |> assign(
21 | :courses,
22 | Enum.map(Accounts.list_courses(), fn m -> [key: m.name, value: m.id] end)
23 | )}
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/user_live/edit.html.heex:
--------------------------------------------------------------------------------
1 | <.live_component module={AtomicWeb.UserLive.FormComponent} user={@user} id={@current_user.id} courses={@courses} title={@page_title} action={@live_action} return_to={~p"/"} />
2 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/user_live/form_component.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserLive.FormComponent do
2 | use AtomicWeb, :live_component
3 |
4 | alias Atomic.Accounts
5 |
6 | @impl true
7 | def update(%{user: user} = assigns, socket) do
8 | changeset = Accounts.update_user(user)
9 |
10 | {:ok,
11 | socket
12 | |> assign(assigns)
13 | |> assign(:changeset, changeset)
14 | |> allow_upload(:profile_picture, accept: ~w(.jpg .jpeg .png), max_entries: 1)}
15 | end
16 |
17 | @impl true
18 | def handle_event("validate", %{"user" => _user_params}, socket) do
19 | {:noreply, socket}
20 | end
21 |
22 | @impl true
23 | def handle_event("save", %{"user" => user_params}, socket) do
24 | update_user(
25 | socket,
26 | socket.assigns.action,
27 | user_params
28 | )
29 | end
30 |
31 | defp update_user(socket, :edit, user_params) do
32 | user = socket.assigns.user
33 |
34 | consume_uploaded_entries(socket, :profile_picture, fn %{path: path}, entry ->
35 | Accounts.update_user_picture(user, %{
36 | "profile_picture" => %Plug.Upload{
37 | content_type: entry.client_type,
38 | filename: entry.client_name,
39 | path: path
40 | }
41 | })
42 | end)
43 |
44 | case Accounts.update_user(user, user_params) do
45 | {:ok, _user} ->
46 | {:noreply,
47 | socket
48 | |> put_flash(:success, "User updated successfully")
49 | |> push_navigate(to: socket.assigns.return_to)}
50 |
51 | {:error, %Ecto.Changeset{} = changeset} ->
52 | {:noreply, assign(socket, :changeset, changeset)}
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/atomic_web/live/user_live/form_component.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
{@title}
3 |
4 | <.form :let={f} for={@changeset} id="user-form" phx-change="validate" phx-target={@myself} phx-submit="save">
5 | {label(f, :name)}
6 | {text_input(f, :name)}
7 | {label(f, :course_id)}
8 | {select(f, :course_id, @courses)}
9 | {label(f, :profile_picture)}
10 | <.live_file_input upload={@uploads.profile_picture} />
11 | {submit("Save", phx_disable_with: "Saving...")}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/lib/atomic_web/plugs/authorize.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Plugs.Authorize do
2 | @moduledoc """
3 | This plug is used to authorize users to access certain parts of the application.
4 | """
5 | import Plug.Conn
6 |
7 | alias Atomic.Organizations
8 |
9 | def init(opts), do: opts
10 |
11 | def call(conn, :master = role) do
12 | if conn.assigns.current_user.role == role do
13 | conn
14 | else
15 | conn
16 | |> send_resp(:not_found, "")
17 | |> halt()
18 | end
19 | end
20 |
21 | def call(conn, minimum_authorized_role) do
22 | if authorized?(conn, minimum_authorized_role) do
23 | conn
24 | else
25 | conn
26 | |> send_resp(:not_found, "")
27 | |> halt()
28 | end
29 | end
30 |
31 | defp authorized?(conn, minimum_authorized_role) do
32 | organization_id = get_organization_id(conn)
33 |
34 | case {organization_id, conn.assigns.current_user} do
35 | {nil, _} ->
36 | false
37 |
38 | {id, user} ->
39 | user_can_manage_organization?(user, id, minimum_authorized_role)
40 | end
41 | end
42 |
43 | defp user_can_manage_organization?(user, organization_id, minimum_authorized_role) do
44 | user_organizations = Enum.map(user.organizations, & &1.id)
45 | role = Organizations.get_role(user.id, organization_id)
46 | allowed_roles = Organizations.roles_bigger_than_or_equal(minimum_authorized_role)
47 |
48 | (organization_id in user_organizations && role in allowed_roles) || user.role == :master
49 | end
50 |
51 | defp get_organization_id(conn) do
52 | case conn.params["organization_id"] do
53 | organization_id when is_binary(organization_id) ->
54 | organization_id
55 |
56 | _ ->
57 | nil
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/atomic_web/plugs/verify_association.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Plugs.VerifyAssociation do
2 | @moduledoc """
3 | This plug is used to confirm if the object being accessed has an association with the organization in the connection parameters.
4 | """
5 | import Plug.Conn
6 |
7 | def init(opts), do: opts
8 |
9 | def call(conn, fun) do
10 | case conn.params["id"] do
11 | id when is_binary(id) ->
12 | case fun.(id) do
13 | nil ->
14 | conn
15 | |> send_resp(:not_found, "")
16 | |> halt()
17 |
18 | entity ->
19 | verify_association(entity, conn)
20 | end
21 |
22 | _ ->
23 | conn
24 | end
25 | end
26 |
27 | defp verify_association(entity, conn) do
28 | if has_relation?(entity, conn.params["organization_id"]) do
29 | conn
30 | else
31 | conn
32 | |> send_resp(:not_found, "")
33 | |> halt()
34 | end
35 | end
36 |
37 | defp has_relation?(_, organization_id) when is_nil(organization_id), do: false
38 |
39 | defp has_relation?(entity, organization_id) do
40 | entity.organization_id == organization_id
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/atomic_web/storybook.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Storybook do
2 | @moduledoc """
3 | Storybook configuration for Atomic
4 | """
5 |
6 | use PhoenixStorybook,
7 | otp_app: :atomic_web,
8 | content_path: Path.expand("../../storybook", __DIR__),
9 | # assets path are remote path, not local file-system paths
10 | css_path: "/assets/storybook.css",
11 | js_path: "/assets/storybook.js",
12 | sandbox_class: "atomic-web"
13 | end
14 |
--------------------------------------------------------------------------------
/lib/atomic_web/templates/error/404.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <.live_title>
9 | {assigns[:page_title] || "Atomic"}
10 |
11 |
12 |
13 |
14 |
15 |
16 | Error
17 | 404
18 |
19 |
Page Not Found
20 | <.link href="/" class="bg-primary-500 my-4 inline-flex items-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-primary-600 focus-visible:outline-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2">
21 | {gettext("Go back home")}
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lib/atomic_web/templates/error/500.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <.live_title>
9 | {assigns[:page_title] || "Atomic"}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
500
17 |
Internal Server Error
18 |
Oops! It appears that something went wrong on our end.
19 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lib/atomic_web/templates/layout/_live_navbar.html.heex:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/lib/atomic_web/templates/layout/_user_menu.html.heex:
--------------------------------------------------------------------------------
1 | <%= if @current_user do %>
2 | {@current_user.email}
3 | {link("Log out", to: ~p"/users/log_out", method: :delete)}
4 | <% else %>
5 | {link("Register", to: ~p"/users/register")}
6 | {link("Log in", to: ~p"/users/log_in")}
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/lib/atomic_web/templates/layout/app.html.heex:
--------------------------------------------------------------------------------
1 |
2 | <%= if Phoenix.Flash.get(@flash, :info) do %>
3 |
4 |
5 |
6 |
7 |
8 |
9 | <.icon name="hero-information-circle-solid" class="h-6 w-6 text-blue-400" />
10 |
11 |
12 | {Phoenix.Flash.get(@flash, :info)}
13 |
14 |
15 |
16 |
17 |
18 | <% end %>
19 |
20 | <%= if Phoenix.Flash.get(@flash, :error) do %>
21 |
22 |
23 |
24 |
25 |
26 |
27 | <.icon name="hero-x-circle-solid" class="h-6 w-6 text-red-400" />
28 |
29 |
30 | {Phoenix.Flash.get(@flash, :error)}
31 |
32 |
33 |
34 |
35 |
36 | <% end %>
37 |
38 | {@inner_content}
39 |
40 |
--------------------------------------------------------------------------------
/lib/atomic_web/templates/layout/live.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= for {key, message} <- @flash do %>
4 | <.live_component id={key} module={AtomicWeb.Components.Notification} type={key} message={message} flash={@flash} />
5 | <% end %>
6 |
7 |
8 |
9 |
10 |
11 |
12 | {render("_live_navbar.html", assigns)}
13 |
14 |
15 |
16 | {@inner_content}
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/lib/atomic_web/templates/layout/root.html.heex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {csrf_meta_tag()}
10 | <.live_title>
11 | {assigns[:page_title] || "Atomic"}
12 |
13 |
14 |
16 |
17 |
18 | {@inner_content}
19 |
20 |
21 |
--------------------------------------------------------------------------------
/lib/atomic_web/templates/user_confirmation/new.html.heex:
--------------------------------------------------------------------------------
1 | Resend confirmation instructions
2 |
3 | <.form :let={f} for={%{}} as={: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", to: ~p"/users/log_in")}
14 |
15 |
--------------------------------------------------------------------------------
/lib/atomic_web/views/email_view.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.EmailView do
2 | use AtomicWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/atomic_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.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(AtomicWeb.Gettext, "errors", msg, msg, count, opts)
44 | else
45 | Gettext.dgettext(AtomicWeb.Gettext, "errors", msg, opts)
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/atomic_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.ErrorView do
2 | use AtomicWeb, :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/atomic_web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.LayoutView do
2 | use AtomicWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/atomic_web/views/pdf_view.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.PDFView do
2 | use AtomicWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/atomic_web/views/user_change_password_view.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserChangePasswordView do
2 | use AtomicWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/atomic_web/views/user_confirmation_view.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserConfirmationView do
2 | use AtomicWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/atomic_web/views/user_reset_password_view.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserResetPasswordView do
2 | use AtomicWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/atomic_web/views/user_setup_view.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserSetupView do
2 | use AtomicWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/linux.yml:
--------------------------------------------------------------------------------
1 | services:
2 | db:
3 | network_mode: "host"
4 | web:
5 | network_mode: "host"
6 |
--------------------------------------------------------------------------------
/priv/fake/masters.txt:
--------------------------------------------------------------------------------
1 | Chandler Bing
2 | Monica Geller
3 | Ross Geller
4 | Joey Tribbiani
5 | Rachel Green
6 | Phoebe Buffay
--------------------------------------------------------------------------------
/priv/fake/students.txt:
--------------------------------------------------------------------------------
1 | Aberforth Dumbledore
2 | Adrian Mole
3 | Albus Dumbledore
4 | Albus Sverus Potter
5 | Amycus Carrow
6 | Anne Frank
7 | Anne of Green Gables
8 | Argus Filch
9 | Asterix Obelix
10 | Calvin and Hobbes
11 | Charity Burbage
12 | Charlie Brown
13 | Corto Maltese
14 | Curious George
15 | Dudley Dursley
16 | Elphias Doge
17 | Ernie Macmillan
18 | Filius Fitwick
19 | Fleur Delacour
20 | Gabrielle Delacour
21 | George Weasley
22 | Geronimo Stilton
23 | Gilderoy Lockhart
24 | Greg Heffley
25 | Gregory Goyle
26 | Hannah Abott
27 | Harry Potter
28 | Heidi and Marco
29 | Helena Ravenclaw
30 | Hermione Ganger
31 | Horce Slughorn
32 | Huckleberry Finn
33 | Hungry Catterpilar
34 | James Potter
35 | Katie Bell
36 | King Babar
37 | Lilly Evans Potter
38 | Lily Luna Potter
39 | Little Prince
40 | Lucious Malfoy
41 | Lucky Luck
42 | Luna Lovegood
43 | Mafalda Quino
44 | Malala Malala
45 | Marry Cattermole
46 | Michael Corner
47 | Molly Weasley
48 | Nancy Drew
49 | Narcissa Malfoy
50 | Neville Longbottom
51 | Nymphadora Tonks
52 | Padington Bear
53 | Padma Patil
54 | Pansy Parkinson
55 | Peppa Pig
56 | Percy Weasley
57 | Peter Rabbit
58 | Petunia Evans Dursley
59 | Pippi Longstocking
60 | Pomona Sprout
61 | Reginald Cattermole
62 | Reginald Coner
63 | Remus Lupin
64 | Rita Skeeter
65 | Rubeus Hagrid
66 | Rufus Scrimgeour
67 | Rupert Bear
68 | Scooby Doo
69 | Serlock Holmes
70 | Snoopy Dog
71 | Stuart Little
72 | Ted Tonks
73 | Throwfinn Rowle
74 | Tintin Herge
75 | Tom Sawyer
76 | Vernon Dursley
77 | Viktor Krum
78 | Vincent Crabbe
79 | Winnie de Pooh
--------------------------------------------------------------------------------
/priv/repo/migrations/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto_sql],
3 | inputs: ["*.exs"]
4 | ]
5 |
--------------------------------------------------------------------------------
/priv/repo/migrations/2022000000000_create_organizations.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreateOrganizations do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:organizations, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 |
8 | add :name, :string, null: false
9 | add :long_name, :string, null: false
10 | add :description, :text, null: false
11 |
12 | add :logo, :string
13 | add :location, :map
14 |
15 | timestamps()
16 | end
17 |
18 | create unique_index(:organizations, [:name])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20221000000000_create_departments.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreateDepartments do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:departments, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 |
8 | add :name, :string, null: false
9 | add :description, :text
10 |
11 | add :collaborator_applications, :boolean, default: false, null: false
12 | add :archived, :boolean, default: false, null: false
13 |
14 | add :banner, :string
15 |
16 | add :organization_id, references(:organizations, on_delete: :delete_all, type: :binary_id),
17 | null: false
18 |
19 | timestamps()
20 | end
21 |
22 | create index(:departments, [:organization_id])
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20221014155230_create_users_auth_tables.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreateUsersAuthTables do
2 | use Ecto.Migration
3 |
4 | def change do
5 | execute "CREATE EXTENSION IF NOT EXISTS citext", ""
6 |
7 | create table(:users, primary_key: false) do
8 | add :id, :binary_id, primary_key: true
9 |
10 | add :name, :string
11 | add :email, :citext, null: false
12 | add :slug, :citext
13 | add :role, :string, null: false, default: "student"
14 |
15 | add :socials, :map
16 |
17 | add :hashed_password, :string, null: false
18 |
19 | add :confirmed_at, :naive_datetime
20 | add :phone_number, :string
21 | add :profile_picture, :string
22 | add :banner, :string
23 |
24 | add :current_organization_id,
25 | references(:organizations, type: :binary_id, on_delete: :delete_all)
26 |
27 | timestamps()
28 | end
29 |
30 | create unique_index(:users, [:email])
31 | create unique_index(:users, [:slug])
32 |
33 | create table(:users_tokens, primary_key: false) do
34 | add :id, :binary_id, primary_key: true
35 |
36 | add :token, :binary, null: false
37 | add :context, :string, null: false
38 | add :sent_to, :string
39 |
40 | add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false
41 |
42 | timestamps(updated_at: false)
43 | end
44 |
45 | create index(:users_tokens, [:user_id])
46 | create unique_index(:users_tokens, [:context, :token])
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20221022010100_create_activities.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreateActivities do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:activities, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 |
8 | add :title, :string, null: false
9 | add :description, :text, null: false
10 |
11 | add :start, :naive_datetime, null: false
12 | add :finish, :naive_datetime, null: false
13 |
14 | add :minimum_entries, :integer, null: false
15 | add :maximum_entries, :integer, null: false
16 | add :enrolled, :integer, default: 0, null: false
17 |
18 | add :image, :string
19 | add :location, :map
20 |
21 | add :organization_id, references(:organizations, type: :binary_id), null: false
22 |
23 | timestamps()
24 | end
25 |
26 | create constraint(:activities, :enrolled_less_than_max, check: "enrolled <= maximum_entries")
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20221104160002_create_enrollments.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreateEnrollments do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:enrollments, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 |
8 | add :present, :boolean, null: false, default: false
9 |
10 | add :activity_id, references(:activities, on_delete: :delete_all, type: :binary_id)
11 | add :user_id, references(:users, type: :binary_id)
12 |
13 | timestamps()
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20221123000537_create_partners.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreatePartners do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:partners, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 |
8 | add :name, :string, null: false
9 | add :description, :text
10 | add :notes, :text
11 |
12 | add :benefits, :text
13 | add :archived, :boolean, default: false
14 | add :image, :string
15 |
16 | add :location, :map
17 | add :socials, :map
18 |
19 | add :organization_id, references(:organizations, on_delete: :delete_all, type: :binary_id)
20 |
21 | timestamps()
22 | end
23 |
24 | create index(:partners, [:organization_id])
25 | create unique_index(:partners, [:name])
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20230313102641_create_memberships.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreateMemberships do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:memberships, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 |
8 | add :role, :string, null: false
9 |
10 | add :user_id, references(:users, type: :binary_id), null: false
11 | add :organization_id, references(:organizations, type: :binary_id), null: false
12 |
13 | timestamps()
14 | end
15 |
16 | create unique_index(:memberships, [:user_id, :organization_id])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20230325151547_create_courses.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreateCourses do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:courses, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 |
8 | add :name, :string, null: false
9 | add :cycle, :string, null: false
10 |
11 | timestamps()
12 | end
13 |
14 | alter table(:users) do
15 | add :course_id, references(:courses, type: :binary_id)
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20230830011755_create_announcements.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreateAnnouncements do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:announcements, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 | add :title, :string, null: false
8 | add :description, :text, null: false
9 | add :image, :string
10 |
11 | add :organization_id, references(:organizations, on_delete: :nothing, type: :binary_id),
12 | null: false
13 |
14 | timestamps()
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20230880102641_create_collaborators.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreateCollaborators do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:collaborators, primary_key: false) do
6 | add :id, :binary_id, primary_key: true
7 | add :accepted, :boolean, default: false
8 | add :accepted_at, :naive_datetime
9 |
10 | add :user_id, references(:users, on_delete: :nothing, type: :binary_id), null: false
11 |
12 | add :department_id, references(:departments, on_delete: :delete_all, type: :binary_id),
13 | null: false
14 |
15 | timestamps()
16 | end
17 |
18 | create unique_index(:collaborators, [:user_id, :department_id])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20231111204142_create_posts.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Migrations.CreatePosts do
2 | @moduledoc false
3 | use Ecto.Migration
4 |
5 | def change do
6 | create table(:posts, primary_key: false) do
7 | add :id, :binary_id, primary_key: true
8 |
9 | add :type, :string, null: false
10 |
11 | timestamps()
12 | end
13 |
14 | create index(:posts, [:inserted_at, :id])
15 |
16 | alter table(:activities) do
17 | add :post_id, references(:posts, type: :binary_id), null: false
18 | end
19 |
20 | alter table(:announcements) do
21 | add :post_id, references(:posts, type: :binary_id), null: false
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Seeds do
2 | @moduledoc """
3 | Script for populating the database.
4 | You can run it as:
5 | $ mix run priv/repo/seeds.exs # or mix ecto.seed
6 | """
7 | @seeds_dir "priv/repo/seeds"
8 |
9 | def run do
10 | [
11 | "organizations.exs",
12 | "courses.exs",
13 | "accounts.exs",
14 | "feed.exs",
15 | "enrollments.exs",
16 | "departments.exs",
17 | "memberships.exs",
18 | "partners.exs"
19 | ]
20 | |> Enum.each(fn file ->
21 | Code.require_file("#{@seeds_dir}/#{file}")
22 | end)
23 | end
24 | end
25 |
26 | Atomic.Repo.Seeds.run()
27 |
--------------------------------------------------------------------------------
/priv/repo/seeds/accounts.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Seeds.Accounts do
2 | @moduledoc """
3 | Seeds the database with users.
4 | """
5 | alias Atomic.Accounts
6 | alias Atomic.Accounts.{Course, User}
7 | alias Atomic.Organizations.Organization
8 | alias Atomic.Repo
9 |
10 | @masters File.read!("priv/fake/masters.txt") |> String.split("\n")
11 | @students File.read!("priv/fake/students.txt") |> String.split("\n")
12 |
13 | def run do
14 | case Repo.all(User) do
15 | [] ->
16 | seed_users(@masters, :master)
17 | seed_users(@students, :student)
18 |
19 | _ ->
20 | Mix.shell().error("Found users, aborting seeding users.")
21 | end
22 | end
23 |
24 | def seed_users(characters, role) do
25 | courses = Repo.all(Course)
26 | organizations = Repo.all(Organization)
27 |
28 | for character <- characters do
29 | email = (character |> String.downcase() |> String.replace(~r/\s*/, "")) <> "@mail.pt"
30 | slug = character |> String.downcase() |> String.replace(~r/\s/, "_")
31 |
32 | phone_number =
33 | "+3519#{Enum.random([1, 2, 3, 6])}#{for _ <- 1..7, do: Enum.random(0..9) |> Integer.to_string()}"
34 |
35 | user = %{
36 | "name" => character,
37 | "email" => email,
38 | "slug" => slug,
39 | "phone_number" => phone_number,
40 | "password" => "password1234",
41 | "role" => role,
42 | "course_id" => Enum.random(courses).id,
43 | "current_organization_id" => Enum.random(organizations).id
44 | }
45 |
46 | case Accounts.register_user(user) do
47 | {:ok, changeset} ->
48 | Repo.update!(Accounts.User.confirm_changeset(changeset))
49 |
50 | {:error, changeset} ->
51 | Mix.shell().error(Kernel.inspect(changeset.errors))
52 | end
53 | end
54 | end
55 | end
56 |
57 | Atomic.Repo.Seeds.Accounts.run()
58 |
--------------------------------------------------------------------------------
/priv/repo/seeds/courses.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Seeds.Courses do
2 | @moduledoc """
3 | Seeds the database with courses.
4 | """
5 | alias Atomic.Accounts
6 | alias Atomic.Accounts.Course
7 | alias Atomic.Repo
8 |
9 | @courses File.read!("data/courses.txt") |> String.split("\n")
10 | @cycles ~w(Bachelors Masters PhD)a
11 |
12 | def run do
13 | case Repo.all(Course) do
14 | [] ->
15 | seed_courses()
16 |
17 | _ ->
18 | Mix.shell().error("Found courses, aborting seeding courses.")
19 | end
20 | end
21 |
22 | def seed_courses do
23 | @courses
24 | |> Enum.each(fn course ->
25 | %{
26 | name: course,
27 | cycle: Enum.random(@cycles)
28 | }
29 | |> Accounts.create_course()
30 | end)
31 | end
32 | end
33 |
34 | Atomic.Repo.Seeds.Courses.run()
35 |
--------------------------------------------------------------------------------
/priv/repo/seeds/enrollments.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Seeds.Enrollments do
2 | @moduledoc """
3 | Seeds the database with enrollments.
4 | """
5 | alias Atomic.Accounts.User
6 | alias Atomic.Activities
7 | alias Atomic.Activities.{Activity, Enrollment}
8 | alias Atomic.Repo
9 |
10 | def run do
11 | seed_enrollments()
12 | end
13 |
14 | def seed_enrollments do
15 | case Repo.all(Enrollment) do
16 | [] ->
17 | users = Repo.all(User)
18 | activities = Repo.all(Activity)
19 |
20 | for user <- users do
21 | for _ <- 1..Enum.random(1..2) do
22 | Activities.create_enrollment(
23 | Enum.random(activities).id,
24 | user
25 | )
26 | end
27 | end
28 |
29 | _ ->
30 | Mix.shell().error("Found enrollments, aborting seeding enrollments.")
31 | end
32 | end
33 | end
34 |
35 | Atomic.Repo.Seeds.Enrollments.run()
36 |
--------------------------------------------------------------------------------
/priv/repo/seeds/memberships.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Seeds.Memberships do
2 | @moduledoc """
3 | Seeds the database with memberships.
4 | """
5 | alias Atomic.Accounts.User
6 | alias Atomic.Organization
7 | alias Atomic.Organizations.{Membership, Organization}
8 | alias Atomic.Repo
9 |
10 | @roles Membership.roles()
11 |
12 | def run do
13 | seed_memberships()
14 | end
15 |
16 | def seed_memberships do
17 | case Repo.all(Membership) do
18 | [] ->
19 | users = Repo.all(User)
20 | organizations = Repo.all(Organization)
21 |
22 | for user <- users do
23 | random_number = :rand.uniform(100)
24 |
25 | # 50% chance of having a membership
26 | if random_number < 50 do
27 | %Membership{}
28 | |> Membership.changeset(%{
29 | "user_id" => user.id,
30 | "organization_id" => Enum.random(organizations).id,
31 | "role" => Enum.random(@roles)
32 | })
33 | |> Repo.insert!()
34 | end
35 | end
36 |
37 | _ ->
38 | Mix.shell().error("Found memberships, aborting seeding memberships.")
39 | end
40 | end
41 | end
42 |
43 | Atomic.Repo.Seeds.Memberships.run()
44 |
--------------------------------------------------------------------------------
/priv/repo/seeds/partners.exs:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Repo.Seeds.Partners do
2 | @moduledoc """
3 | Seeds the database with partners.
4 | """
5 | alias Atomic.Organizations.{Organization, Partner}
6 | alias Atomic.Repo
7 |
8 | def run do
9 | case Repo.all(Partner) do
10 | [] ->
11 | seed_partners()
12 |
13 | _ ->
14 | Mix.shell().error("Found partners, aborting seeding partners.")
15 | end
16 | end
17 |
18 | def seed_partners do
19 | organizations = Repo.all(Organization)
20 |
21 | location = %{
22 | name: Faker.Address.city(),
23 | url: Faker.Internet.url()
24 | }
25 |
26 | socials = %{
27 | instagram: Faker.Internet.slug(),
28 | facebook: Faker.Internet.slug(),
29 | x: Faker.Internet.slug(),
30 | youtube: Faker.Internet.slug(),
31 | tiktok: Faker.Internet.slug(),
32 | website: Faker.Internet.url()
33 | }
34 |
35 | for {organization, i} <- Enum.with_index(organizations) do
36 | for _ <- 0..5 do
37 | %Partner{}
38 | |> Partner.changeset(%{
39 | name: Faker.Company.name() <> " " <> Integer.to_string(i),
40 | description: Enum.join(Faker.Lorem.paragraphs(2), "\n"),
41 | benefits: Enum.join(Faker.Lorem.paragraphs(5), "\n"),
42 | organization_id: organization.id,
43 | location: location,
44 | socials: socials
45 | })
46 | |> Repo.insert!()
47 | end
48 | end
49 | end
50 | end
51 |
52 | Atomic.Repo.Seeds.Partners.run()
53 |
--------------------------------------------------------------------------------
/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cesium/atomic/3456c7fce5cca238cc837676fc840aafcfd06089/priv/static/favicon.ico
--------------------------------------------------------------------------------
/priv/static/images/backgrounds/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cesium/atomic/3456c7fce5cca238cc837676fc840aafcfd06089/priv/static/images/backgrounds/0.png
--------------------------------------------------------------------------------
/priv/static/images/cesium-ORANGE.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | CeSIUM
10 |
14 |
18 |
22 |
26 |
30 |
34 |
39 |
43 |
47 |
48 |
--------------------------------------------------------------------------------
/priv/static/images/facebook.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/priv/static/images/instagram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/priv/static/images/pitch/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cesium/atomic/3456c7fce5cca238cc837676fc840aafcfd06089/priv/static/images/pitch/0.png
--------------------------------------------------------------------------------
/priv/static/images/pitch/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cesium/atomic/3456c7fce5cca238cc837676fc840aafcfd06089/priv/static/images/pitch/1.png
--------------------------------------------------------------------------------
/priv/static/images/tiktok.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/priv/static/images/x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/priv/static/images/youtube.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | # Admin routes
3 | Disallow: /organizations/new
4 | Disallow: /organizations/*/edit
5 | Disallow: /activities/*/edit
6 | Disallow: /announcements/*/edit
7 | Disallow: /departments/*/edit
8 | Disallow: /partners/*/edit
9 |
10 | # Authentication and account management
11 | Disallow: /users/
12 | Disallow: /users/register
13 | Disallow: /users/log_in
14 | Disallow: /users/log_out
15 | Disallow: /users/reset_password
16 | Disallow: /users/change_password
17 | Disallow: /users/setup
18 | Disallow: /users/confirm
19 | Disallow: /users/confirm_email/*
20 |
21 | # Tools
22 | Disallow: /scanner/
23 | Disallow: /storybook/
24 | Disallow: /dev/
25 | Disallow: /dashboard/
26 | Disallow: /mailbox/
27 |
28 | Disallow: /*?*
29 | Allow: /
30 | Sitemap: https://atomic.cesium.pt/sitemap.xml
31 | Crawl-delay: 10
--------------------------------------------------------------------------------
/scripts/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright © 2021 Nelson Estevão
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the “Software”), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/scripts/colors.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -n
2 |
3 | # shellcheck disable=SC2034
4 | RED=$(tput setaf 1)
5 | # shellcheck disable=SC2034
6 | GREEN=$(tput setaf 2)
7 | # shellcheck disable=SC2034
8 | YELLOW=$(tput setaf 3)
9 | # shellcheck disable=SC2034
10 | CYAN=$(tput setaf 4)
11 | # shellcheck disable=SC2034
12 | PURPLE=$(tput setaf 5)
13 | # shellcheck disable=SC2034
14 | BLUE=$(tput setaf 6)
15 | # shellcheck disable=SC2034
16 | WHITE=$(tput setaf 7)
17 | # shellcheck disable=SC2034
18 | BOLD=$(tput bold)
19 | # shellcheck disable=SC2034
20 | UNDERLINE=$(tput smul)
21 | # shellcheck disable=SC2034
22 | REVERSE=$(tput rev)
23 | # shellcheck disable=SC2034
24 | RESET=$(tput sgr0)
25 |
--------------------------------------------------------------------------------
/scripts/git.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -Eeuo pipefail
4 |
5 | SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]:-$0}")
6 |
7 | . "${SCRIPT_DIR}/helpers.sh"
8 |
9 | function get_default_repo_branch() {
10 | # Check if main exists and use instead of master
11 | if command git rev-parse --git-dir &>/dev/null; then
12 | for ref in refs/{heads,remotes/{origin,upstream}}/{main,trunk,mainline,default}; do
13 | if command git show-ref -q --verify "$ref"; then
14 | echo "${ref##*/}"
15 | return
16 | fi
17 | done
18 | fi
19 | echo master
20 | }
21 |
22 | ([ "$0" = "${BASH_SOURCE[0]}" ] && display_version 0.14.0) || true
23 |
24 | default_branch=$(get_default_repo_branch)
25 | echo "The default repo branch is ${default_branch}"
26 |
--------------------------------------------------------------------------------
/scripts/helpers.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -Eeuo pipefail
4 |
5 | import() {
6 | local -r SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]:-$0}")
7 |
8 | # shellcheck source=/dev/null
9 | . "${SCRIPTS_DIR}/${1}"
10 | }
11 |
12 | # shellcheck source=./colors.sh
13 | import colors.sh
14 |
15 | function display_version() {
16 | local program="${2:-$(basename "$0")}"
17 | local version=${1:?"You need to give a version number"}
18 |
19 | if [ -x "$(command -v figlet)" ]; then
20 | echo -n "${BLUE}${BOLD}"
21 | figlet "${program} script"
22 | echo -n "${RESET}"
23 | echo "version ${version}"
24 | else
25 | echo "${program} script version ${version}"
26 | fi
27 | }
28 |
29 | function help_title_section() {
30 | local -r TITLE=$(echo "$@" | tr '[:lower:]' '[:upper:]')
31 | echo -e "${BOLD}${TITLE}${RESET}"
32 | }
33 |
34 | ([ "$0" = "${BASH_SOURCE[0]}" ] && display_version 0.14.0) || true
35 |
--------------------------------------------------------------------------------
/storybook/_root.index.exs:
--------------------------------------------------------------------------------
1 | defmodule Storybook.Root do
2 | # See https://hexdocs.pm/phoenix_storybook/PhoenixStorybook.Index.html for full index
3 | # documentation.
4 |
5 | use PhoenixStorybook.Index
6 |
7 | def folder_icon, do: {:fa, "book-open", :light, "lsb-mr-1"}
8 | def folder_name, do: "Atomic"
9 |
10 | def entry("begin") do
11 | [
12 | name: "Welcome",
13 | icon: {:fa, "hand-wave", :thin}
14 | ]
15 | end
16 |
17 | def entry("icons") do
18 | [
19 | name: "Icons List",
20 | icon: {:fa, "icons", :thin}
21 | ]
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/storybook/begin.story.exs:
--------------------------------------------------------------------------------
1 | defmodule Storybook.Begin do
2 | use PhoenixStorybook.Story, :page
3 |
4 | def doc, do: "Set of reusable components used in the Atomic platform by CeSIUM."
5 |
6 | def render(assigns) do
7 | ~H"""
8 |
9 |
10 |
11 |
Atomic
12 |
13 |
14 | """
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/storybook/components/_components.index.exs:
--------------------------------------------------------------------------------
1 | defmodule Storybook.Components do
2 | use PhoenixStorybook.Index
3 |
4 | def folder_icon, do: {:fa, "toolbox", :light, "lsb-mr-1"}
5 | def folder_name, do: "Components"
6 | def folder_open?, do: true
7 | end
8 |
--------------------------------------------------------------------------------
/storybook/components/empty.story.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Storybook.Components.Empty do
2 | use PhoenixStorybook.Story, :component
3 |
4 | alias AtomicWeb.Components.Empty
5 |
6 | def function, do: &Empty.empty_state/1
7 |
8 | def variations do
9 | [
10 | %Variation{
11 | id: :default,
12 | attributes: %{
13 | placeholder: "item",
14 | url: "#"
15 | }
16 | }
17 | ]
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/storybook/components/gradient.story.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Storybook.Components.Gradient do
2 | use PhoenixStorybook.Story, :component
3 |
4 | alias AtomicWeb.Components.Gradient
5 |
6 | def function, do: &Gradient.gradient/1
7 |
8 | def template do
9 | """
10 |
11 | <.lsb-variation/>
12 |
13 | """
14 | end
15 |
16 | def variations do
17 | [
18 | %Variation{
19 | id: :random
20 | },
21 | %Variation{
22 | id: :predictable,
23 | attributes: %{
24 | seed: "CAOS"
25 | }
26 | }
27 | ]
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/storybook/components/icon.story.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Storybook.Components.Icon do
2 | use PhoenixStorybook.Story, :component
3 |
4 | alias AtomicWeb.Components.Icon
5 |
6 | def function, do: &Icon.icon/1
7 |
8 | def template do
9 | """
10 |
11 | <.lsb-variation />
12 |
13 | """
14 | end
15 |
16 | def variations do
17 | [
18 | %Variation{
19 | id: :hero_outline,
20 | description: "Heroicon outline",
21 | attributes: %{
22 | name: "hero-academic-cap"
23 | }
24 | },
25 | %Variation{
26 | id: :hero_solid,
27 | description: "Heroicon solid",
28 | attributes: %{
29 | name: "hero-academic-cap-solid"
30 | }
31 | },
32 | %Variation{
33 | id: :hero_mini,
34 | description: "Heroicon mini",
35 | attributes: %{
36 | name: "hero-academic-cap-mini"
37 | }
38 | },
39 | %Variation{
40 | id: :hero_micro,
41 | description: "Heroicon micro",
42 | attributes: %{
43 | name: "hero-academic-cap-micro"
44 | }
45 | },
46 | %Variation{
47 | id: :tabler_outline,
48 | description: "Tabler outline",
49 | attributes: %{
50 | name: "tabler-affiliate"
51 | }
52 | },
53 | %Variation{
54 | id: :tabler_filled,
55 | description: "Tabler filled",
56 | attributes: %{
57 | name: "tabler-affiliate-filled"
58 | }
59 | }
60 | ]
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/storybook/components/map.story.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Storybook.Components.Map do
2 | use PhoenixStorybook.Story, :component
3 |
4 | alias AtomicWeb.Components.Map
5 |
6 | def function, do: &Map.map/1
7 |
8 | def variations do
9 | [
10 | %Variation{
11 | id: :default,
12 | attributes: %{
13 | location: "Centro de Estudantes de Engenharia Informática"
14 | }
15 | },
16 | %VariationGroup{
17 | id: :type,
18 | description: "Type",
19 | variations: [
20 | %Variation{
21 | id: :normal,
22 | attributes: %{
23 | location: "Universidade do Minho - Campus de Gualtar",
24 | type: :normal
25 | }
26 | },
27 | %Variation{
28 | id: :satellite,
29 | attributes: %{
30 | location: "Universidade do Minho - Campus de Gualtar",
31 | type: :satellite
32 | }
33 | }
34 | ]
35 | },
36 | %Variation{
37 | id: :zoom,
38 | attributes: %{
39 | location: "Núcleo de Informática da AEFEUP",
40 | zoom: 7
41 | }
42 | },
43 | %Variation{
44 | id: :controls,
45 | attributes: %{
46 | location: "Braga, Portugal",
47 | controls: true
48 | }
49 | }
50 | ]
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/storybook/components/spinner.story.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Storybook.Components.Spinner do
2 | use PhoenixStorybook.Story, :component
3 |
4 | alias AtomicWeb.Components.Spinner
5 |
6 | def function, do: &Spinner.spinner/1
7 |
8 | def variations do
9 | [
10 | %Variation{
11 | id: :default
12 | },
13 | %VariationGroup{
14 | id: :sizes,
15 | description: "Different sizes",
16 | variations: [
17 | %Variation{
18 | id: :extra_small,
19 | attributes: %{
20 | size: :xs
21 | }
22 | },
23 | %Variation{
24 | id: :small,
25 | attributes: %{
26 | size: :sm
27 | }
28 | },
29 | %Variation{
30 | id: :medium,
31 | attributes: %{
32 | size: :md
33 | }
34 | },
35 | %Variation{
36 | id: :large,
37 | attributes: %{
38 | size: :lg
39 | }
40 | },
41 | %Variation{
42 | id: :extra_large,
43 | attributes: %{
44 | size: :xl
45 | }
46 | }
47 | ]
48 | },
49 | %VariationGroup{
50 | id: :colors,
51 | description: "Colors",
52 | variations: [
53 | %Variation{
54 | id: :red,
55 | attributes: %{
56 | size: :md,
57 | class: "text-primary-500"
58 | }
59 | },
60 | %Variation{
61 | id: :small,
62 | attributes: %{
63 | size: :lg,
64 | class: "text-secondary-500"
65 | }
66 | }
67 | ]
68 | }
69 | ]
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/storybook/components/tabs.story.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Storybook.Components.Tabs do
2 | use PhoenixStorybook.Story, :component
3 |
4 | alias AtomicWeb.Components.Icon
5 | alias AtomicWeb.Components.Tabs
6 |
7 | def function, do: &Tabs.tabs/1
8 |
9 | def imports, do: [{Tabs, tab: 1}, {Icon, icon: 1}]
10 |
11 | def variations do
12 | [
13 | %Variation{
14 | id: :simple,
15 | slots: [
16 | """
17 | <.tab active={true}>
18 | All
19 |
20 | <.tab>
21 | Following
22 |
23 | """
24 | ]
25 | },
26 | %Variation{
27 | id: :with_numbers,
28 | slots: [
29 | """
30 | <.tab active={true} number={5}>
31 | All
32 |
33 | <.tab number={2}>
34 | Following
35 |
36 | """
37 | ]
38 | },
39 | %Variation{
40 | id: :disabled,
41 | slots: [
42 | """
43 | <.tab active={true}>
44 | All
45 |
46 | <.tab disabled={true}>
47 | Following
48 |
49 | """
50 | ]
51 | },
52 | %Variation{
53 | id: :custom_class,
54 | slots: [
55 | """
56 | <.tab active={true} class="bg-red-100 text-red-600">
57 | All
58 |
59 | <.tab class="bg-blue-100 text-blue-600">
60 | Following
61 |
62 | """
63 | ]
64 | },
65 | %Variation{
66 | id: :with_icon,
67 | slots: [
68 | """
69 | <.tab active={true}>
70 | <.icon name="hero-home" class="size-5 mr-2" />
71 | All
72 |
73 | <.tab>
74 | <.icon name="hero-star" class="size-5 mr-2" />
75 | Following
76 |
77 | """
78 | ]
79 | }
80 | ]
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/storybook/components/unauthenticated.story.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.Storybook.Components.Unauthenticated do
2 | use PhoenixStorybook.Story, :component
3 |
4 | alias AtomicWeb.Components.Unauthenticated
5 |
6 | def function, do: &Unauthenticated.unauthenticated_state/1
7 |
8 | def variations do
9 | [
10 | %Variation{
11 | id: :default,
12 | attributes: %{}
13 | }
14 | ]
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/atomic_web/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.PageControllerTest do
2 | use AtomicWeb.ConnCase
3 |
4 | setup :register_and_log_in_user
5 |
6 | test "GET /", %{conn: conn} do
7 | conn = get(conn, "/")
8 |
9 | assert html_response(conn, 200) =~ "Home"
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/atomic_web/controllers/user_registration_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.UserRegistrationControllerTest do
2 | use AtomicWeb.ConnCase, async: true
3 |
4 | describe "GET /users/register" do
5 | test "renders registration page", %{conn: conn} do
6 | conn = get(conn, ~p"/users/register")
7 | response = html_response(conn, 200)
8 | assert response =~ "Register for an account"
9 | assert response =~ "Log in"
10 | assert response =~ "Register"
11 | end
12 |
13 | test "redirects if already logged in", %{conn: conn} do
14 | conn = conn |> log_in_user(insert(:user)) |> get(~p"/users/register")
15 | assert redirected_to(conn) == "/"
16 | end
17 | end
18 |
19 | describe "POST /users/register" do
20 | @tag :capture_log
21 | test "creates account but doesn't log the user in", %{conn: conn} do
22 | user_attrs = %{
23 | name: Faker.Person.name(),
24 | email: Faker.Internet.email(),
25 | role: "student",
26 | password: "password1234"
27 | }
28 |
29 | conn =
30 | post(conn, ~p"/users/register", %{
31 | "user" => user_attrs
32 | })
33 |
34 | assert is_nil(get_session(conn, :user_token))
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/atomic_web/helpers_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.HelpersTest do
2 | @moduledoc """
3 | Tests for the AtomicWeb.Helpers module
4 | """
5 | use ExUnit.Case, async: true
6 |
7 | import AtomicWeb.Helpers
8 |
9 | doctest AtomicWeb.Helpers
10 | end
11 |
--------------------------------------------------------------------------------
/test/atomic_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.ErrorViewTest do
2 | use AtomicWeb.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(AtomicWeb.ErrorView, "404.html.heex", []) == "Not Found"
9 | end
10 |
11 | test "renders 500.html" do
12 | assert render_to_string(AtomicWeb.ErrorView, "500.html.heex", []) == "Internal Server Error"
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test/atomic_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.LayoutViewTest do
2 | use AtomicWeb.ConnCase, async: true
3 |
4 | # When testing helpers, you may want to import Phoenix.HTML and
5 | # use functions such as safe_to_string() to convert the helper
6 | # result into an HTML string.
7 | # import Phoenix.HTML
8 | end
9 |
--------------------------------------------------------------------------------
/test/atomic_web/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.PageViewTest do
2 | use AtomicWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule AtomicWeb.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 AtomicWeb.ChannelCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 | alias Ecto.Adapters.SQL.Sandbox
20 |
21 | using do
22 | quote do
23 | # Import conveniences for testing with channels
24 | import Phoenix.ChannelTest
25 | import AtomicWeb.ChannelCase
26 |
27 | # The default endpoint for testing
28 | @endpoint AtomicWeb.Endpoint
29 | end
30 | end
31 |
32 | setup tags do
33 | pid = Sandbox.start_owner!(Atomic.Repo, shared: not tags[:async])
34 | on_exit(fn -> Sandbox.stop_owner(pid) end)
35 | :ok
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.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 Atomic.DataCase, async: true`, although
14 | this option is not recommended for other databases.
15 | """
16 |
17 | use ExUnit.CaseTemplate
18 |
19 | alias Ecto.Adapters.SQL
20 |
21 | using do
22 | quote do
23 | alias Atomic.Repo
24 |
25 | import Ecto
26 | import Ecto.Changeset
27 | import Ecto.Query
28 | import Atomic.DataCase
29 | end
30 | end
31 |
32 | setup tags do
33 | Atomic.DataCase.setup_sandbox(tags)
34 | :ok
35 | end
36 |
37 | @doc """
38 | Sets up the sandbox based on the test tags.
39 | """
40 | def setup_sandbox(tags) do
41 | pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Atomic.Repo, shared: not tags[:async])
42 | on_exit(fn -> SQL.Sandbox.stop_owner(pid) end)
43 | end
44 |
45 | @doc """
46 | A helper that transforms changeset errors into a map of messages.
47 |
48 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
49 | assert "password is too short" in errors_on(changeset).password
50 | assert %{password: ["password is too short"]} = errors_on(changeset)
51 |
52 | """
53 | def errors_on(changeset) do
54 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
55 | Regex.replace(~r"%{(\w+)}", message, fn _, key ->
56 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
57 | end)
58 | end)
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/test/support/factories/accounts_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Factories.AccountFactory do
2 | @moduledoc """
3 | A factory to generate account related structs
4 | """
5 |
6 | alias Atomic.Accounts.User
7 |
8 | defmacro __using__(_opts) do
9 | quote do
10 | @roles User.roles()
11 |
12 | def user_factory do
13 | %User{
14 | name: Faker.Person.name(),
15 | email: Faker.Internet.email(),
16 | slug: Faker.Internet.user_name(),
17 | role: Enum.random(@roles),
18 | hashed_password: Bcrypt.hash_pwd_salt("password1234")
19 | }
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/support/factories/activities_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Factories.ActivityFactory do
2 | @moduledoc """
3 | A factory to generate account related structs
4 | """
5 | alias Atomic.Activities.{Activity, Enrollment}
6 |
7 | defmacro __using__(_opts) do
8 | quote do
9 | def activity_factory do
10 | organization = insert(:organization)
11 |
12 | %Activity{
13 | title: Faker.Beer.brand(),
14 | description: Faker.Lorem.paragraph(),
15 | minimum_entries: Enum.random(1..10),
16 | maximum_entries: Enum.random(11..20),
17 | enrolled: 0,
18 | start: NaiveDateTime.utc_now(),
19 | finish: NaiveDateTime.utc_now() |> NaiveDateTime.add(1, :hour),
20 | organization_id: organization.id,
21 | post: build(:post, type: "activity")
22 | }
23 | end
24 |
25 | def enrollment_factory do
26 | %Enrollment{
27 | present: Enum.random([true, false]),
28 | activity: build(:activity),
29 | user: build(:user)
30 | }
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/test/support/factories/departments_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Factories.DepartmentFactory do
2 | @moduledoc """
3 | A factory to generate account related structs
4 | """
5 |
6 | alias Atomic.Organizations.Department
7 |
8 | defmacro __using__(_opts) do
9 | quote do
10 | @departments [
11 | "Pedagogical Department",
12 | "CAOS Department",
13 | "Department of Image",
14 | "Department of Partnerships",
15 | "Recreational Department"
16 | ]
17 |
18 | def department_factory do
19 | organization = insert(:organization)
20 |
21 | %Department{
22 | name: Enum.random(@departments),
23 | organization_id: organization.id
24 | }
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/support/factories/feed_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Factories.FeedFactory do
2 | @moduledoc """
3 | A factory to generate feed related structs
4 | """
5 | alias Atomic.Feed.Post
6 |
7 | defmacro __using__(_opts) do
8 | quote do
9 | def post_factory do
10 | %Post{
11 | type: Enum.random([:activity, :announcement])
12 | }
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/support/factories/organizations_factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Factories.OrganizationFactory do
2 | @moduledoc """
3 | A factory to generate organization related structs.
4 | """
5 | alias Atomic.Organizations.{
6 | Announcement,
7 | Membership,
8 | Organization
9 | }
10 |
11 | defmacro __using__(_opts) do
12 | quote do
13 | @roles Membership.roles()
14 |
15 | def organization_factory do
16 | %Organization{
17 | name: Faker.Company.name(),
18 | long_name: Faker.Company.name(),
19 | description: Faker.Lorem.paragraph()
20 | }
21 | end
22 |
23 | def membership_factory do
24 | %Membership{
25 | user: build(:user),
26 | organization: build(:organization),
27 | role: Enum.random(@roles)
28 | }
29 | end
30 |
31 | def announcement_factory do
32 | organization = insert(:organization)
33 |
34 | %Announcement{
35 | title: Faker.Company.buzzword(),
36 | description: Faker.Lorem.paragraph(),
37 | post: build(:post, type: "announcement"),
38 | organization_id: organization.id
39 | }
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/test/support/factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.Factory do
2 | @moduledoc false
3 | use ExMachina.Ecto, repo: Atomic.Repo
4 |
5 | use Atomic.Factories.{
6 | AccountFactory,
7 | ActivityFactory,
8 | DepartmentFactory,
9 | FeedFactory,
10 | OrganizationFactory
11 | }
12 | end
13 |
--------------------------------------------------------------------------------
/test/support/fixtures/accounts_fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.AccountsFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `Atomic.Accounts` context.
5 | """
6 |
7 | def unique_user_email, do: "user#{System.unique_integer()}@example.com"
8 | def valid_user_password, do: "password1234"
9 |
10 | def valid_user_attributes(attrs \\ %{}) do
11 | Enum.into(attrs, %{
12 | email: unique_user_email(),
13 | password: valid_user_password()
14 | })
15 | end
16 |
17 | def user_fixture(attrs \\ %{}) do
18 | {:ok, user} =
19 | attrs
20 | |> valid_user_attributes()
21 | |> Atomic.Accounts.register_user()
22 |
23 | user
24 | end
25 |
26 | def extract_user_token(fun) do
27 | {:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
28 |
29 | body =
30 | if captured_email.html_body do
31 | captured_email.html_body
32 | else
33 | captured_email.text_body
34 | end
35 |
36 | [_, token | _] = String.split(body, "[TOKEN]")
37 | token
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/test/support/fixtures/activities_fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.ActivitiesFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `Atomic.Activities` context.
5 | """
6 |
7 | alias Atomic.OrganizationsFixtures
8 |
9 | @doc """
10 | Generate a activity.
11 | """
12 | def activity_fixture(attrs \\ %{}) do
13 | {:ok, activity} =
14 | attrs
15 | |> Enum.into(%{
16 | description: "some description",
17 | title: "some title",
18 | maximum_entries: 42,
19 | minimum_entries: 0,
20 | finish: ~N[2022-10-22 20:00:00],
21 | start: ~N[2022-10-22 20:00:00],
22 | organization_id: OrganizationsFixtures.organization_fixture().id
23 | })
24 | |> Atomic.Activities.create_activity_with_post()
25 |
26 | activity
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/support/fixtures/feed_fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.FeedFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `Atomic.Feed` context.
5 | """
6 |
7 | @doc """
8 | Generate a post.
9 | """
10 | def post_fixture(attrs \\ %{}) do
11 | {:ok, post} =
12 | attrs
13 | |> Enum.into(%{
14 | type: "activity"
15 | })
16 | |> Atomic.Feed.create_post()
17 |
18 | post
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/support/fixtures/organizations_fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.OrganizationsFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `Atomic.Organizations` context.
5 | """
6 |
7 | @doc """
8 | Generate an organization.
9 | """
10 | def organization_fixture(attrs \\ %{}) do
11 | {:ok, organization} =
12 | attrs
13 | |> Enum.into(%{
14 | description: "some description",
15 | name: "SN",
16 | long_name: "some name"
17 | })
18 | |> Atomic.Organizations.create_organization()
19 |
20 | organization
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/test/support/fixtures/partnerships_fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule Atomic.PartnersFixtures do
2 | @moduledoc """
3 | This module defines test helpers for creating
4 | entities via the `Atomic.Partners` context.
5 | """
6 |
7 | alias Atomic.OrganizationsFixtures
8 |
9 | @doc """
10 | Generate a partner.
11 | """
12 | def partner_fixture(attrs \\ %{}) do
13 | {:ok, partner} =
14 | attrs
15 | |> Enum.into(%{
16 | description: "some description",
17 | name: "some name",
18 | organization_id: OrganizationsFixtures.organization_fixture().id
19 | })
20 | |> Atomic.Partners.create_partner()
21 |
22 | partner
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 | Ecto.Adapters.SQL.Sandbox.mode(Atomic.Repo, :manual)
3 | {:ok, _} = Application.ensure_all_started(:ex_machina)
4 |
--------------------------------------------------------------------------------