Skip to content

Commit

Permalink
Compute user streak
Browse files Browse the repository at this point in the history
  • Loading branch information
Loïc Knuchel committed Nov 19, 2023
1 parent 63b947c commit bf05b86
Show file tree
Hide file tree
Showing 25 changed files with 219 additions and 83 deletions.
2 changes: 1 addition & 1 deletion backend/assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = {
],
safelist: [
{
pattern: /(bg|text)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-.*/,
pattern: /(bg|text|border|from|via|to)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-.*/,
},
],
theme: {
Expand Down
4 changes: 2 additions & 2 deletions backend/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ config :azimutt,
free_plan_seats: 3,
# MUST stay in sync with frontend/src/Conf.elm (`features`)
free_plan_layouts: 3,
free_plan_memos: 5,
free_plan_memos: 3,
free_plan_groups: 1,
free_plan_colors: false,
free_plan_private_links: true,
free_plan_private_links: false,
free_plan_sql_export: false,
free_plan_db_analysis: false,
free_plan_db_access: false,
Expand Down
24 changes: 21 additions & 3 deletions backend/lib/azimutt/organizations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule Azimutt.Organizations do
alias Azimutt.Projects.ProjectToken
alias Azimutt.Repo
alias Azimutt.Services.StripeSrv
alias Azimutt.Tracking
alias Azimutt.Utils.Enumx
alias Azimutt.Utils.Result

Expand Down Expand Up @@ -329,7 +330,7 @@ defmodule Azimutt.Organizations do
|> Repo.update()
end

def get_organization_plan(%Organization{} = organization) do
def get_organization_plan(%Organization{} = organization, maybe_current_user) do
plans = Azimutt.config(:instance_plans) || ["free"]

cond do
Expand All @@ -338,7 +339,7 @@ defmodule Azimutt.Organizations do
organization.stripe_subscription_id && StripeSrv.stripe_configured?() -> stripe_plan(plans, organization.stripe_subscription_id)
true -> default_plan(plans)
end
|> Result.map(fn plan -> plan_overrides(plans, organization, plan) end)
|> Result.map(fn plan -> plan_overrides(plans, organization, plan, maybe_current_user) end)
end

defp clever_cloud_plan(plans, %CleverCloud.Resource{} = resource) do
Expand Down Expand Up @@ -378,7 +379,7 @@ defmodule Azimutt.Organizations do
end
end

defp plan_overrides(plans, %Organization{} = organization, %OrganizationPlan{} = plan) do
defp plan_overrides(plans, %Organization{} = organization, %OrganizationPlan{} = plan, maybe_current_user) do
if organization.data != nil && plans |> Enum.member?("pro") do
plan
|> override_layouts(organization.data)
Expand All @@ -389,6 +390,7 @@ defmodule Azimutt.Organizations do
else
plan
end
|> override_streak(maybe_current_user)
end

defp override_layouts(%OrganizationPlan{} = plan, %Organization.Data{} = data) do
Expand Down Expand Up @@ -431,6 +433,22 @@ defmodule Azimutt.Organizations do
end
end

defp override_streak(%OrganizationPlan{} = plan, %User{} = maybe_current_user) do
# MUST stay sync with backend/lib/azimutt_web/templates/partials/_streak.html.heex
streak = Tracking.get_streak(maybe_current_user) |> Result.or_else(0)
plan = %{plan | streak: streak}
plan = if(streak >= 4, do: %{plan | colors: true}, else: plan)
plan = if(streak >= 6, do: %{plan | memos: nil}, else: plan)
plan = if(streak >= 10, do: %{plan | layouts: nil}, else: plan)
plan = if(streak >= 15, do: %{plan | groups: nil}, else: plan)
plan = if(streak >= 25, do: %{plan | sql_export: true}, else: plan)
plan = if(streak >= 40, do: %{plan | db_analysis: true}, else: plan)
plan = if(streak >= 60, do: %{plan | private_links: true}, else: plan)
plan
end

defp override_streak(%OrganizationPlan{} = plan, maybe_current_user) when is_nil(maybe_current_user), do: plan

defp best_limit(a, b) do
cond do
a == nil || b == nil -> nil
Expand Down
9 changes: 6 additions & 3 deletions backend/lib/azimutt/organizations/organization_plan.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Azimutt.Organizations.OrganizationPlan do
use TypedStruct
alias Azimutt.Organizations.OrganizationPlan

# MUST stay in sync with frontend/src/Models/Plan.elm
# MUST stay in sync with frontend/ts-src/types/organization.ts & frontend/src/Models/Plan.elm
typedstruct enforce: true do
@derive Jason.Encoder
field :id, atom()
Expand All @@ -16,6 +16,7 @@ defmodule Azimutt.Organizations.OrganizationPlan do
field :sql_export, boolean()
field :db_analysis, boolean()
field :db_access, boolean()
field :streak, integer()
end

def free do
Expand All @@ -30,7 +31,8 @@ defmodule Azimutt.Organizations.OrganizationPlan do
private_links: Azimutt.config(:free_plan_private_links),
sql_export: Azimutt.config(:free_plan_sql_export),
db_analysis: Azimutt.config(:free_plan_db_analysis),
db_access: Azimutt.config(:free_plan_db_access)
db_access: Azimutt.config(:free_plan_db_access),
streak: 0
}
end

Expand All @@ -45,7 +47,8 @@ defmodule Azimutt.Organizations.OrganizationPlan do
private_links: true,
sql_export: true,
db_analysis: true,
db_access: true
db_access: true,
streak: 0
}
end
end
6 changes: 3 additions & 3 deletions backend/lib/azimutt/projects.ex
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ defmodule Azimutt.Projects do
end)
end

def access_project(project_id, token_id, now) do
def access_project(project_id, token_id, maybe_current_user, now) do
ProjectToken
|> where(
[pt],
Expand All @@ -210,14 +210,14 @@ defmodule Azimutt.Projects do
|> where([p, _, _], p.id == ^project_id and p.storage_kind == :remote)
|> Repo.one()
|> Result.from_nillable()
|> Result.filter(fn p -> Organizations.get_organization_plan(p.organization) |> Result.exists(& &1.private_links) end, :not_found)
|> Result.filter(fn p -> Organizations.get_organization_plan(p.organization, maybe_current_user) |> Result.exists(& &1.private_links) end, :not_found)
|> Result.tap(fn _ -> token |> ProjectToken.access_changeset(now) |> Repo.update() end)
end)
end

def load_project(project_id, maybe_current_user, token_id, now) do
if token_id do
access_project(project_id, token_id, now)
access_project(project_id, token_id, maybe_current_user, now)
else
get_project(project_id, maybe_current_user)
end
Expand Down
24 changes: 24 additions & 0 deletions backend/lib/azimutt/tracking.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ defmodule Azimutt.Tracking do
alias Azimutt.Utils.Nil
alias Azimutt.Utils.Result

def get_streak(%User{} = current_user) do
now = DateTime.utc_now()
months_ago = Timex.shift(now, months: -4)

Event
|> where([e], e.created_by_id == ^current_user.id and e.created_at >= ^months_ago)
|> select([e], {fragment("to_char(?, 'yyyy-mm-dd')", e.created_at), count(e.id, :distinct)})
|> group_by([e], fragment("to_char(?, 'yyyy-mm-dd')", e.created_at))
|> Repo.all()
|> Result.from_nillable()
|> Result.map(fn res -> compute_streak(Map.new(res), now, 0) end)
end

defp compute_streak(activity, now, streak) do
if Map.has_key?(activity, Date.to_string(now)) do
compute_streak(activity, Timex.shift(now, days: -1), streak + 1)
else
streak
end
end

def last_used_project(%User{} = current_user) do
Event
|> where(
Expand Down Expand Up @@ -99,6 +120,9 @@ defmodule Azimutt.Tracking do
def user_onboarding(%User{} = current_user, step, data),
do: create_event("user_onboarding", user_data(current_user), data |> Map.put("step", step), current_user, nil, nil)

def organization_loaded(%User{} = current_user, %Organization{} = org),
do: create_event("organization_loaded", org_data(org), nil, current_user, org.id, nil)

def project_created(%User{} = current_user, %Project{} = project),
do: create_event("project_created", project_data(project), nil, current_user, project.organization.id, project.id)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule AzimuttWeb.Admin.OrganizationController do
{:ok, start_stats} = "2022-11-01" |> Timex.parse("{YYYY}-{0M}-{0D}")

with {:ok, %Organization{} = organization} <- Admin.get_organization(organization_id),
{:ok, %OrganizationPlan{} = plan} <- Organizations.get_organization_plan(organization) do
{:ok, %OrganizationPlan{} = plan} <- Organizations.get_organization_plan(organization, nil) do
conn
|> render("show.html",
now: now,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule AzimuttWeb.Api.OrganizationController do
current_user = conn.assigns.current_user
ctx = CtxParams.from_params(params)
organizations = Organizations.list_organizations(current_user)
conn |> render("index.json", organizations: organizations, ctx: ctx)
conn |> render("index.json", organizations: organizations, current_user: current_user, ctx: ctx)
end

def table_colors(conn, %{"organization_organization_id" => organization_id, "tweet_url" => tweet_url}) do
Expand Down
4 changes: 2 additions & 2 deletions backend/lib/azimutt_web/controllers/api/project_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ defmodule AzimuttWeb.Api.ProjectController do
current_user = conn.assigns.current_user

with {:ok, %Organization{} = organization} <- Organizations.get_organization(organization_id, current_user),
do: conn |> render("index.json", projects: organization.projects)
do: conn |> render("index.json", projects: organization.projects, current_user: current_user)
end

def show(conn, %{"organization_id" => _organization_id, "project_id" => project_id} = params) do
Expand All @@ -32,7 +32,7 @@ defmodule AzimuttWeb.Api.ProjectController do
ctx = CtxParams.from_params(params)

with {:ok, %Project{} = project} <- Projects.load_project(project_id, maybe_current_user, params["token"], now),
do: conn |> render("show.json", project: project, ctx: ctx)
do: conn |> render("show.json", project: project, maybe_current_user: maybe_current_user, ctx: ctx)
end

swagger_path :create do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule AzimuttWeb.OrganizationBillingController do
use AzimuttWeb, :controller
require Logger
alias Azimutt.Accounts
alias Azimutt.Accounts.User
alias Azimutt.CleverCloud
alias Azimutt.Heroku
alias Azimutt.Organizations
Expand All @@ -28,42 +29,42 @@ defmodule AzimuttWeb.OrganizationBillingController do
cond do
organization.clever_cloud_resource -> conn |> redirect(external: CleverCloud.app_addons_url())
organization.heroku_resource -> conn |> redirect(external: Heroku.app_addons_url(organization.heroku_resource.app))
organization.stripe_subscription_id -> conn |> stripe_subscription_view(organization)
true -> generate_billing_view(conn, "subscribe.html", organization, "You haven't got subscribe yet !")
organization.stripe_subscription_id -> conn |> stripe_subscription_view(organization, current_user)
true -> generate_billing_view(conn, "subscribe.html", organization, current_user, "You haven't got subscribe yet !")
end
end
end

defp stripe_subscription_view(conn, %Organization{} = organization) do
defp stripe_subscription_view(conn, %Organization{} = organization, %User{} = current_user) do
case Organizations.get_subscription_status(organization.stripe_subscription_id) do
{:ok, :active} ->
generate_billing_view(conn, "billing.html", organization, "Your subscription is active !")
generate_billing_view(conn, "billing.html", organization, current_user, "Your subscription is active !")

{:ok, :past_due} ->
generate_billing_view(conn, "billing.html", organization, "We have an issue with your subscription")
generate_billing_view(conn, "billing.html", organization, current_user, "We have an issue with your subscription")

{:ok, :unpaid} ->
generate_billing_view(conn, "billing.html", organization, "We have an issue with your subscription")
generate_billing_view(conn, "billing.html", organization, current_user, "We have an issue with your subscription")

{:ok, :canceled} ->
generate_billing_view(conn, "subscribe.html", organization, "Your subscription is canceled")
generate_billing_view(conn, "subscribe.html", organization, current_user, "Your subscription is canceled")

{:ok, :incomplete} ->
generate_billing_view(conn, "billing.html", organization, "We have an issue with your subscription")
generate_billing_view(conn, "billing.html", organization, current_user, "We have an issue with your subscription")

{:ok, :incomplete_expired} ->
generate_billing_view(conn, "billing.html", organization, "We have an issue with your subscription")
generate_billing_view(conn, "billing.html", organization, current_user, "We have an issue with your subscription")

{:ok, :trialing} ->
generate_billing_view(conn, "billing.html", organization, "You are in free trial")
generate_billing_view(conn, "billing.html", organization, current_user, "You are in free trial")

{:error, err} ->
conn |> put_flash(:error, "Can't show view: #{err}.") |> redirect(to: Routes.organization_path(conn, :show, organization))
end
end

defp generate_billing_view(conn, file, organization, message) do
with {:ok, plan} <- Organizations.get_organization_plan(organization) do
defp generate_billing_view(conn, file, organization, %User{} = current_user, message) do
with {:ok, plan} <- Organizations.get_organization_plan(organization, current_user) do
conn
|> put_view(AzimuttWeb.OrganizationView)
|> render(file, organization: organization, plan: plan, message: message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ defmodule AzimuttWeb.OrganizationController do
end

with {:ok, organization} <- Organizations.get_organization(organization_id, current_user),
{:ok, _event} = Tracking.organization_loaded(current_user, organization),
projects = Projects.list_projects(organization, current_user),
{:ok, organization_events} = Tracking.recent_organization_events(organization),
{:ok, plan} <- Organizations.get_organization_plan(organization),
{:ok, plan} <- Organizations.get_organization_plan(organization, current_user),
do: render(conn, "show.html", organization: organization, projects: projects, plan: plan, organization_events: organization_events)
end

Expand All @@ -58,7 +59,7 @@ defmodule AzimuttWeb.OrganizationController do
end

with {:ok, organization} <- Organizations.get_organization(organization_id, current_user),
{:ok, plan} <- Organizations.get_organization_plan(organization) do
{:ok, plan} <- Organizations.get_organization_plan(organization, current_user) do
changeset = Organization.update_changeset(organization, %{}, current_user)
render(conn, "edit.html", organization: organization, plan: plan, changeset: changeset)
end
Expand All @@ -75,7 +76,7 @@ defmodule AzimuttWeb.OrganizationController do
|> redirect(to: Routes.organization_path(conn, :show, organization))

{:error, %Ecto.Changeset{} = changeset} ->
with {:ok, plan} <- Organizations.get_organization_plan(organization),
with {:ok, plan} <- Organizations.get_organization_plan(organization, current_user),
do: render(conn, "edit.html", organization: organization, plan: plan, changeset: changeset)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ defmodule AzimuttWeb.OrganizationInvitationController do

def show(conn, %{"invitation_id" => invitation_id}) do
now = DateTime.utc_now()
current_user = conn.assigns.current_user
# FIXME: remove `get_organization_plan`
{:ok, invitation} = Organizations.get_organization_invitation(invitation_id)
organization = invitation.organization
{:ok, plan} = Organizations.get_organization_plan(organization)
{:ok, plan} = Organizations.get_organization_plan(organization, current_user)

render(conn, "show.html",
now: now,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule AzimuttWeb.OrganizationMemberController do
use AzimuttWeb, :controller
alias Azimutt.Accounts
alias Azimutt.Accounts.User
alias Azimutt.Organizations
alias Azimutt.Organizations.Organization
alias Azimutt.Organizations.OrganizationInvitation
Expand All @@ -20,7 +21,7 @@ defmodule AzimuttWeb.OrganizationMemberController do

with {:ok, %Organization{} = organization} <- Organizations.get_organization(organization_id, current_user) do
organization_invitation_changeset = OrganizationInvitation.create_changeset(%OrganizationInvitation{}, %{}, organization.id, current_user, now)
render_index(conn, organization, organization_invitation_changeset)
render_index(conn, organization, current_user, organization_invitation_changeset)
end
end

Expand All @@ -45,7 +46,7 @@ defmodule AzimuttWeb.OrganizationMemberController do
|> redirect(to: Routes.organization_member_path(conn, :index, invitation.organization_id))

{:error, %Ecto.Changeset{} = changeset} ->
render_index(conn, organization, changeset)
render_index(conn, organization, current_user, changeset)
end
end

Expand Down Expand Up @@ -90,8 +91,8 @@ defmodule AzimuttWeb.OrganizationMemberController do
end
end

defp render_index(conn, organization, changeset) do
with {:ok, plan} <- Organizations.get_organization_plan(organization) do
defp render_index(conn, organization, %User{} = current_user, changeset) do
with {:ok, plan} <- Organizations.get_organization_plan(organization, current_user) do
# FIXME: create a `Organizations.get_pending_invitations(organization.id)`
organization_invitations =
organization.invitations
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,4 @@
<div class="min-h-full pr-4 bg-gray-50 sm:pr-6 lg:pr-8 lg:flex-shrink-0 lg:border-l lg:border-gray-200 xl:pr-0">
<div class="pl-6 lg:w-80">
<div class="pt-6 pb-2">
<h2 class="text-sm font-semibold">Activities</h2>
<ul role="list" class="space-y-6">
<%= for event <- @organization_events do %>
<li class="relative flex gap-x-4">
<div class="absolute top-0 left-0 flex justify-center w-6 -bottom-6">
<div class="w-px bg-gray-200"></div>
</div>
<img src={event.created_by.avatar} alt="" class="relative flex-none w-6 h-6 mt-3 rounded-full bg-gray-50">
<p class="flex-auto py-0.5 text-xs leading-5 text-gray-500">
<span class="font-medium text-gray-900">
<%= AzimuttWeb.OrganizationView.generate_html_event_description(event) %>
</span>
</p>
<time datetime={event.created_at} class="flex-none py-0.5 text-xs leading-5 text-gray-500"> <%= AzimuttWeb.OrganizationView.last_update(event.created_at) %> </time>
</li>
<% end %>
</ul>

</div>
</div>
<div class="min-h-full pr-4 bg-gray-50 sm:pr-6 lg:pr-8 lg:flex-shrink-0 lg:border-l lg:border-gray-200 xl:pr-0 divide-y">
<%= render AzimuttWeb.PartialsView, "_streak.html", value: @plan.streak %>
<%= render AzimuttWeb.PartialsView, "_activity_feed.html", events: @organization_events %>
</div>
Loading

0 comments on commit bf05b86

Please sign in to comment.