Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: organization pages #500

Draft
wants to merge 18 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 3 additions & 16 deletions lib/atomic/departments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -194,19 +194,6 @@ defmodule Atomic.Departments do
Repo.all(Collaborator)
end

@doc """
Returns the list of collaborators belonging to an organization.

## Examples

iex> list_collaborators_by_organization_id("99d7c9e5-4212-4f59-a097-28aaa33c2621")
[%Collaborator{}, ...]

"""
def list_collaborators_by_organization_id(id) do
Repo.all(from p in Collaborator, where: p.organization_id == ^id)
end

@doc """
Gets a single collaborator.

Expand Down Expand Up @@ -363,14 +350,14 @@ defmodule Atomic.Departments do

## Examples

iex> list_collaborators_by_department_id("99d7c9e5-4212-4f59-a097-28aaa33c2621")
iex> list_department_collaborators(123)
[%Collaborator{}, ...]

"""
def list_collaborators_by_department_id(id, opts \\ []) do
def list_department_collaborators(id, opts \\ []) do
Collaborator
|> apply_filters(opts)
|> where([c], c.department_id == ^id)
|> apply_filters(opts)
|> Repo.all()
end

Expand Down
3 changes: 3 additions & 0 deletions lib/atomic/location/location.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ defmodule Atomic.Location do
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
end

def link(location) when is_map_key(location, :url), do: location.url
def link(location), do: "https://www.google.com/maps/search/?api=1&query=#{location.name}"
end
116 changes: 62 additions & 54 deletions lib/atomic/organizations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,25 @@ defmodule Atomic.Organizations do
end

@doc """
Returns the list of organizations where an user is an admin or owner.
Returns the list of organizations which are connected with the user.
By default, it returns the organizations where the user is an admin or owner.

## Examples

iex> list_user_organizations(user_id)
iex> list_user_organizations(123)
[%Organization{}, ...]

iex> list_user_organizations(456)
[]

iex> list_user_organizations(123, [:follower])
[%Organization{}, ...]

"""
def list_user_organizations(user_id, opts \\ []) do
def list_user_organizations(user_id, roles \\ [:admin, :owner], opts \\ []) do
Organization
|> join(:inner, [o], m in Membership, on: m.organization_id == o.id)
|> where([o, m], m.user_id == ^user_id and m.role in [:admin, :owner])
|> where([o, m], m.user_id == ^user_id and m.role in ^roles)
|> apply_filters(opts)
|> Repo.all()
end
Expand Down Expand Up @@ -256,43 +264,51 @@ defmodule Atomic.Organizations do
end

@doc """
Returns the list of memberships.
Returns the list of members in an organization.
A member is someone who is connected to the organization with a role other than `:follower`.

## Examples

iex> list_memberships(%{"organization_id" => id})
[%Organization{}, ...]

iex> list_memberships(%{"user_id" => id})
iex> list_memberships(123)
[%Organization{}, ...]

"""
def list_memberships(params, preloads \\ [])

def list_memberships(%{"organization_id" => organization_id}, preloads) do
def list_memberships(organization_id, opts \\ []) do
Membership
|> where([a], a.organization_id == ^organization_id and a.role != :follower)
|> where([m], m.organization_id == ^organization_id and m.role != :follower)
|> apply_filters(opts)
|> Repo.all()
|> Repo.preload(preloads)
end

def list_memberships(%{"user_id" => user_id}, preloads) do
@doc """
Counts the number of members in an organization.
A member is someone who is connected to the organization with a role other than `:follower`.

## Examples

iex> count_memberships(123)
5

iex> count_memberships(456)
0

"""
def count_memberships(organization_id) do
Membership
|> where([a], a.user_id == ^user_id)
|> Repo.preload(preloads)
|> Repo.all()
|> where([m], m.organization_id == ^organization_id and m.role != :follower)
|> Repo.count()
end

@doc """
Verifies if an user is a member of an organization.
Verifies if an user is a member of an organization.

## Examples
## Examples

iex> member_of?(user, organization)
true
iex> member_of?(user, organization)
true

iex> member_of?(user, organization)
false
iex> member_of?(user, organization)
false

"""
def member_of?(%User{} = user, %Organization{} = organization) do
Expand All @@ -301,6 +317,27 @@ defmodule Atomic.Organizations do
|> Repo.exists?()
end

@doc """
Checks if an user is following an organization.

## Examples

iex> user_following?(123, 456)
true

iex> user_following?(456, 789)
false

"""
def user_following?(user_id, organization_id) do
Membership
|> where(
[m],
m.user_id == ^user_id and m.organization_id == ^organization_id and m.role == :follower
)
|> Repo.exists?()
end

@doc """
Gets an user role in an organization.

Expand Down Expand Up @@ -436,36 +473,7 @@ defmodule Atomic.Organizations do
|> Enum.drop_while(fn elem -> elem != role end)
end

@doc """
Returns the amount of followers in an organization.

## Examples

iex> count_followers("99d7c9e5-4212-4f59-a097-28aaa33c2621")
5

iex> count_followers("9as7c9e5-4212-4f59-a097-28aaa33c2621")
100_000_000_000_000_000_000_000_000
"""
def count_followers(organization_id) do
Membership
|> where([m], m.organization_id == ^organization_id and m.role == :follower)
|> Repo.aggregate(:count, :id)
end

@doc """
Returns the amount of members in an organization.

## Examples

iex> get_total_organization_members(organization_id)
5

"""
def get_total_organization_members(organization_id) do
from(m in Membership, where: m.organization_id == ^organization_id)
|> Repo.aggregate(:count, :id)
end
## Announcements

@doc """
Returns the list of announcements.
Expand Down
15 changes: 14 additions & 1 deletion lib/atomic/organizations/membership.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule Atomic.Organizations.Membership do
* `admin` - The user can control the organization's departments, activities and partners.
* `follower` - The user is following the organization.

This schema can be further extended to include additional roles, such as `member`.
This schema can be further extended to include additional roles, such as `member` (with even different denominations).
"""
use Atomic.Schema

Expand All @@ -34,6 +34,19 @@ defmodule Atomic.Organizations.Membership do
organization
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> prepare_changes(&maybe_increment_follower_count/1)
end

defp maybe_increment_follower_count(changeset) do
organization_id = get_change(changeset, :organization_id)
role = get_change(changeset, :role)

if organization_id && role && role == :follower do
query = from Organization, where: [id: ^organization_id]
changeset.repo.update_all(query, inc: [follower_count: 1])
end

changeset
end

def roles, do: @roles
Expand Down
22 changes: 14 additions & 8 deletions lib/atomic/organizations/organization.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ defmodule Atomic.Organizations.Organization do
use Atomic.Schema

alias Atomic.Accounts.User
alias Atomic.Location
alias Atomic.Organizations.{Announcement, Department, Membership, Partner}
alias Atomic.Uploaders
alias Atomic.{Socials, Uploaders}

@required_fields ~w(name long_name description)a
@optional_fields ~w()a
@required_fields ~w(name email long_name description)a
@optional_fields ~w(location)a

@derive {
Flop.Schema,
filterable: [],
sortable: [:name],
filterable: [:name],
sortable: [:name, :follower_count],
compound_fields: [search: [:name]],
default_order: %{
order_by: [:name],
Expand All @@ -23,11 +22,18 @@ defmodule Atomic.Organizations.Organization do

schema "organizations" do
field :name, :string
field :email, :string
field :long_name, :string
field :description, :string

field :logo, Uploaders.Logo.Type
embeds_one :location, Location, on_replace: :delete
field :location, :string

# field used to better track the number of followers
# can only be updated by the system and through the memberships schema
field :follower_count, :integer, default: 0

embeds_one :socials, Socials, on_replace: :update

has_many :departments, Department,
on_replace: :delete_if_exists,
Expand All @@ -51,7 +57,7 @@ defmodule Atomic.Organizations.Organization do
def changeset(organization, attrs) do
organization
|> cast(attrs, @required_fields ++ @optional_fields)
|> cast_embed(:location, with: &Location.changeset/2)
|> cast_embed(:socials, with: &Socials.changeset/2)
|> validate_required(@required_fields)
|> unique_constraint(:name)
end
Expand Down
2 changes: 2 additions & 0 deletions lib/atomic/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ defmodule Atomic.Repo do
adapter: Ecto.Adapters.Postgres

use Paginator

def count(query), do: aggregate(query, :count)
end
62 changes: 57 additions & 5 deletions lib/atomic/socials/socials.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
defmodule Atomic.Socials do
@moduledoc """
A socials embedded struct schema.
An embedded schema for social media handles or links.

This schema stores the information just as it is, without any processing.
"""
use Atomic.Schema

@optional_fields ~w(instagram facebook x youtube tiktok website)a
@optional_fields ~w(facebook instagram x linkedin website)a

@derive Jason.Encoder
@primary_key false
embedded_schema do
field :instagram, :string
field :facebook, :string
field :instagram, :string
field :x, :string
field :youtube, :string
field :tiktok, :string
field :linkedin, :string
field :website, :string
end

Expand All @@ -22,4 +23,55 @@ defmodule Atomic.Socials do
|> cast(attrs, @optional_fields)
|> validate_format(:website, ~r{^https?://}, message: "must start with http:// or https://")
end

def link(:facebook, handle), do: "https://facebook.com/#{handle}"
def link(:instagram, handle), do: "https://instagram.com/#{handle}"
def link(:x, handle), do: "https://x.com/#{handle}"
def link(:linkedin, handle), do: "https://linkedin.com/#{handle}"

@doc """
Function providing SVG icons for social media platforms with a default size of 4 tailwind units.

## Examples

iex> Socials.icon(:facebook) |> raw()
...

iex> Socials.icon(:instagram, 6) |> raw()
...

"""
def icon(platform, size \\ 4)

def icon(:facebook, size) do
"""
<svg class="size-#{size}" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" clip-rule="evenodd" />
</svg>
"""
end

def icon(:instagram, size) do
"""
<svg class="size-#{size}" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd" d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z" clip-rule="evenodd" />
</svg>
"""
end

def icon(:x, size) do
"""
<svg class="size-#{size}" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M13.6823 10.6218L20.2391 3H18.6854L12.9921 9.61788L8.44486 3H3.2002L10.0765 13.0074L3.2002 21H4.75404L10.7663 14.0113L15.5685 21H20.8131L13.6819 10.6218H13.6823ZM11.5541 13.0956L10.8574 12.0991L5.31391 4.16971H7.70053L12.1742 10.5689L12.8709 11.5655L18.6861 19.8835H16.2995L11.5541 13.096V13.0956Z" />
</svg>
"""
end

def icon(:linkedin, size) do
"""
<svg class="size-#{size} fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"">
<path d="M18.335 18.339H15.67v-4.177c0-.996-.02-2.278-1.39-2.278-1.389 0-1.601 1.084-1.601 2.205v4.25h-2.666V9.75h2.56v1.17h.035c.358-.674 1.228-1.387 2.528-1.387 2.7 0 3.2 1.778 3.2 4.091v4.715zM7.003 8.575a1.546 1.546 0 01-1.548-1.549 1.548 1.548 0 111.547 1.549zm1.336 9.764H5.666V9.75H8.34v8.589zM19.67 3H4.329C3.593 3 3 3.58 3 4.297v15.406C3 20.42 3.594 21 4.328 21h15.338C20.4 21 21 20.42 21 19.703V4.297C21 3.58 20.4 3 19.666 3h.003z"></path>
</svg>
"""
end
end
Loading