Commit 159c5ed5 authored by Mikko Ahlroth's avatar Mikko Ahlroth

Formatted with 1.6

parent bda5c677
[
inputs: [
"{config,lib,test}/**/*.{ex,exs}",
"mix.exs"
]
]
......@@ -11,8 +11,7 @@ config :code_stats, CodeStatsWeb.Endpoint,
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [mix: ["frontend.watch",
cd: Path.expand("../", __DIR__)]]
watchers: [mix: ["frontend.watch", cd: Path.expand("../", __DIR__)]]
# Watch static and templates for browser reloading.
config :code_stats, CodeStatsWeb.Endpoint,
......@@ -42,7 +41,6 @@ config :code_stats, CodeStats.Repo,
hostname: "localhost",
pool_size: 10
config :appsignal, :config, active: true
# To avoid conflicts with workspaces
......
......@@ -19,10 +19,8 @@ config :code_stats, CodeStatsWeb.Endpoint,
# Do not print debug messages in production
config :logger, level: :error
config :appsignal, :config, active: true
# Finally import the config/prod.secret.exs
# which should be versioned separately.
import_config "prod.secret.exs"
......@@ -19,7 +19,7 @@ defmodule CodeStats.Language.AdminCommands do
Moves all XP to the new language and update users' caches.
"""
@spec alias_language(String.t, String.t) :: nil
@spec alias_language(String.t(), String.t()) :: nil
def alias_language(lang_name, target_name) do
{:ok, language} = Language.get_or_create(lang_name)
{:ok, target} = Language.get_or_create(target_name)
......@@ -71,8 +71,8 @@ defmodule CodeStats.Language.AdminCommands do
^id2str.(language.id),
^id2str.(target.id)
)
]
]
]
)
|> Repo.update_all([])
......@@ -85,7 +85,7 @@ defmodule CodeStats.Language.AdminCommands do
All aliased XP will be moved back to this language.
"""
@spec unalias_language(String.t) :: nil
@spec unalias_language(String.t()) :: nil
def unalias_language(lang_name) do
{:ok, language} = Language.get_or_create(lang_name)
......@@ -104,11 +104,9 @@ defmodule CodeStats.Language.AdminCommands do
|> Repo.update_all(set: [language_id: language.id])
# Update caches for all users (since there's no easy way to separate language points)
(from u in User, select: u)
from(u in User, select: u)
|> Repo.all()
|> Enum.each(
fn user -> User.update_cached_xps(user, true) end
)
|> Enum.each(fn user -> User.update_cached_xps(user, true) end)
# Update frontpage language caches
CacheService.refresh_total_language_xp()
......
......@@ -25,17 +25,19 @@ defmodule CodeStats.Language.CacheService do
end
def init(state) do
:ets.new(@language_xp_cache_table, [:named_table, :set, :public, read_concurrency: true, write_concurrency: true])
:ets.new(@language_xp_cache_table, [
:named_table,
:set,
:public,
read_concurrency: true,
write_concurrency: true
])
refresh_total_language_xp_and_repeat()
{:ok, state}
end
@doc """
Add the given amount of XP for the given language in the total language XP cache.
......@@ -48,7 +50,7 @@ defmodule CodeStats.Language.CacheService do
case :ets.update_counter(@language_xp_cache_table, key, {2, value}, {key, value}) do
new_count when is_integer(new_count) -> new_count
_ -> raise "Updating counter #{inspect key} failed!"
_ -> raise "Updating counter #{inspect(key)} failed!"
end
end
......@@ -56,16 +58,12 @@ defmodule CodeStats.Language.CacheService do
Get the total XP in the system for each language. Returns a list of tuples where the first
element is the language name and the second element is the amount of XP.
"""
@spec get_total_language_xps() :: [{String.t, integer}]
@spec get_total_language_xps() :: [{String.t(), integer}]
def get_total_language_xps() do
:ets.match_object(@language_xp_cache_table, {{:"$1", :total}, :"$2"})
|> Enum.map(fn {{lang_name, :total}, amount} -> {lang_name, amount} end)
end
def handle_info(:refresh_total_language_xp, state) do
refresh_total_language_xp_and_repeat()
{:noreply, state}
......@@ -84,17 +82,22 @@ defmodule CodeStats.Language.CacheService do
# Remove old languages as aliases can otherwise result in duplicate items
:ets.delete_all_objects(@language_xp_cache_table)
most_popular_q = from x in XP,
join: l in Language, on: l.id == x.language_id,
group_by: l.id,
order_by: [desc: sum(x.amount)],
select: {l, sum(x.amount)}
most_popular = case Repo.all(most_popular_q) do
nil -> []
ret -> ret
end
|> Enum.map(fn {lang, amount} -> {{lang.name, :total}, amount} end)
most_popular_q =
from(
x in XP,
join: l in Language,
on: l.id == x.language_id,
group_by: l.id,
order_by: [desc: sum(x.amount)],
select: {l, sum(x.amount)}
)
most_popular =
case Repo.all(most_popular_q) do
nil -> []
ret -> ret
end
|> Enum.map(fn {lang, amount} -> {{lang.name, :total}, amount} end)
:ets.insert(@language_xp_cache_table, most_popular)
end
......
......@@ -7,16 +7,16 @@ defmodule CodeStats.Language do
alias CodeStats.Repo
schema "languages" do
field :name, :string
field(:name, :string)
has_many :xps, CodeStats.Language.XP
has_many(:xps, CodeStats.Language.XP)
# Either a language has many aliases or it is an alias of some other language,
# it cannot be both.
# NOTE: Only 1 level of aliases is supported! That is, you cannot form a chain of
# aliases.
belongs_to :alias_of, __MODULE__
has_many :aliases, __MODULE__, foreign_key: :alias_of_id
belongs_to(:alias_of, __MODULE__)
has_many(:aliases, __MODULE__, foreign_key: :alias_of_id)
timestamps()
end
......@@ -40,29 +40,34 @@ defmodule CodeStats.Language do
Returns ok and the language struct if successful.
"""
@spec get_or_create(String.t) :: {:ok, %__MODULE__{}} | {:error, :unknown}
@spec get_or_create(String.t()) :: {:ok, %__MODULE__{}} | {:error, :unknown}
def get_or_create(language_name) do
# Get-create-get to handle race conditions
get_query = from l in __MODULE__,
where: fragment("LOWER(?)", l.name) == fragment("LOWER(?)", ^language_name),
preload: :alias_of
get_query =
from(
l in __MODULE__,
where: fragment("LOWER(?)", l.name) == fragment("LOWER(?)", ^language_name),
preload: :alias_of
)
case Repo.one(get_query) do
%__MODULE__{} = language -> {:ok, language}
%__MODULE__{} = language ->
{:ok, language}
nil ->
changeset(%__MODULE__{}, %{"name" => language_name})
|> put_assoc(:alias_of, nil)
|> Repo.insert()
|> case do
{:ok, language} -> {:ok, language}
{:ok, language} ->
{:ok, language}
{:error, _} ->
case Repo.one(get_query) do
%__MODULE__{} = language -> {:ok, language}
nil -> {:error, :unknown}
end
end
{:error, _} ->
case Repo.one(get_query) do
%__MODULE__{} = language -> {:ok, language}
nil -> {:error, :unknown}
end
end
end
end
end
......@@ -4,12 +4,12 @@ defmodule CodeStats.User.Machine do
import Ecto.Changeset
schema "machines" do
field :name, :string
field :api_salt, :string
field :active, :boolean
field(:name, :string)
field(:api_salt, :string)
field(:active, :boolean)
belongs_to :user, CodeStats.User
has_many :pulses, CodeStats.User.Pulse
belongs_to(:user, CodeStats.User)
has_many(:pulses, CodeStats.User.Pulse)
timestamps()
end
......
......@@ -13,12 +13,12 @@ defmodule CodeStats.User.PasswordReset do
@token_max_life 4
schema "password_resets" do
field :token, Ecto.UUID
field(:token, Ecto.UUID)
# Virtual field that is only used to fetch the correct user
field :username, :string, virtual: true
field(:username, :string, virtual: true)
belongs_to :user, User
belongs_to(:user, User)
timestamps()
end
......@@ -26,7 +26,7 @@ defmodule CodeStats.User.PasswordReset do
@doc """
Get the maximum life of a password reset token, in hours.
"""
@spec token_max_life() :: Integer.t
@spec token_max_life() :: Integer.t()
def token_max_life(), do: @token_max_life
@doc """
......@@ -43,24 +43,26 @@ defmodule CodeStats.User.PasswordReset do
"""
@spec changeset(%__MODULE__{}, %{}) :: {%Ecto.Changeset{}, %User{} | nil}
def changeset(data, params \\ %{}) do
cset = data
|> cast(params, [:username])
|> validate_required([:username])
|> put_change(:token, Ecto.UUID.generate())
|> unique_constraint(:token)
cset =
data
|> cast(params, [:username])
|> validate_required([:username])
|> put_change(:token, Ecto.UUID.generate())
|> unique_constraint(:token)
case cset.valid? do
false -> {cset, nil}
false ->
{cset, nil}
true ->
username = get_change(cset, :username)
q = from u in User,
where: u.username == ^username
q = from(u in User, where: u.username == ^username)
case Repo.one(q) do
# Invalidate changeset, error message is not shown so it is not needed
nil -> {add_error(cset, :username, ""), nil}
nil ->
{add_error(cset, :username, ""), nil}
%User{} = u ->
{put_change(cset, :user_id, u.id), u}
......
......@@ -6,18 +6,18 @@ defmodule CodeStats.User.Pulse do
schema "pulses" do
# When the Pulse was generated on the client. This is somewhat confusingly named
# "sent_at" for legacy reasons, better name would be "coded_at".
field :sent_at, :utc_datetime
field(:sent_at, :utc_datetime)
# Same value as before, but stored in the client's local timezone offset. This should
# be used for certain aggregations to make the results more useful for the user.
field :sent_at_local, :naive_datetime
field(:sent_at_local, :naive_datetime)
# Original offset from UTC for the sent_at_local timestamp. In minutes.
field :tz_offset, :integer
belongs_to :user, CodeStats.User
belongs_to :machine, CodeStats.User.Machine
field(:tz_offset, :integer)
belongs_to(:user, CodeStats.User)
belongs_to(:machine, CodeStats.User.Machine)
has_many :xps, CodeStats.XP
has_many(:xps, CodeStats.XP)
timestamps()
end
......
......@@ -48,9 +48,7 @@ defmodule CodeStats.User.Terminator do
now = DateTime.utc_now()
earliest_valid = CDateTime.subtract!(now, PasswordReset.token_max_life() * 3600)
(from pr in PasswordReset,
where: pr.inserted_at < ^earliest_valid)
from(pr in PasswordReset, where: pr.inserted_at < ^earliest_valid)
|> Repo.delete_all()
end
end
......@@ -13,14 +13,14 @@ defmodule CodeStats.User do
alias CodeStats.XP
schema "users" do
field :username, :string
field :email, :string
field :password, :string
field :last_cached, :utc_datetime
field :private_profile, :boolean
field :cache, :map
field(:username, :string)
field(:email, :string)
field(:password, :string)
field(:last_cached, :utc_datetime)
field(:private_profile, :boolean)
field(:cache, :map)
has_many :pulses, Pulse
has_many(:pulses, Pulse)
timestamps()
end
......@@ -73,49 +73,57 @@ defmodule CodeStats.User do
def update_cached_xps(user, update_all \\ false) do
update_start_time = DateTime.utc_now()
last_cached = if not update_all and user.last_cached != nil do
user.last_cached
else
{:ok, datetime} = Calendar.DateTime.Parse.rfc3339_utc(@null_datetime)
datetime
end
last_cached =
if not update_all and user.last_cached != nil do
user.last_cached
else
{:ok, datetime} = Calendar.DateTime.Parse.rfc3339_utc(@null_datetime)
datetime
end
# If update_all is given or user cache is empty, don't use any previous cache data
cached_data = %{
languages: %{},
machines: %{},
dates: %{},
caching_duration: 0, # Time taken for the last partial cache update
total_caching_duration: 0 # Time taken for the last full cache update
# Time taken for the last partial cache update
caching_duration: 0,
# Time taken for the last full cache update
total_caching_duration: 0
}
cached_data = case {update_all, user.cache} do
{true, _} -> cached_data
{_, nil} -> cached_data
_ -> unformat_cache_from_db(user.cache)
end
cached_data =
case {update_all, user.cache} do
{true, _} -> cached_data
{_, nil} -> cached_data
_ -> unformat_cache_from_db(user.cache)
end
# Load all of user's new XP plus required associations
xps_q = from x in XP,
join: p in Pulse, on: p.id == x.pulse_id,
where: p.user_id == ^user.id and p.inserted_at >= ^last_cached,
select: {p, x}
xps = case Repo.all(xps_q) do
nil -> []
ret -> ret
end
xps_q =
from(
x in XP,
join: p in Pulse,
on: p.id == x.pulse_id,
where: p.user_id == ^user.id and p.inserted_at >= ^last_cached,
select: {p, x}
)
xps =
case Repo.all(xps_q) do
nil -> []
ret -> ret
end
language_data = generate_language_cache(cached_data.languages, xps)
machine_data = generate_machine_cache(cached_data.machines, xps)
date_data = generate_date_cache(cached_data.dates, xps)
cache_contents =
%{
languages: language_data,
machines: machine_data,
dates: date_data
}
cache_contents = %{
languages: language_data,
machines: machine_data,
dates: date_data
}
# Correct key for storing caching duration
duration_key = if update_all, do: :total_caching_duration, else: :caching_duration
......@@ -169,16 +177,19 @@ defmodule CodeStats.User do
# Format data in cache for storing into db as JSON
defp format_cache_for_db(cache) do
languages = Map.get(cache, :languages)
|> int_keys_to_str()
languages =
Map.get(cache, :languages)
|> int_keys_to_str()
machines = Map.get(cache, :machines)
|> int_keys_to_str()
machines =
Map.get(cache, :machines)
|> int_keys_to_str()
dates = Map.get(cache, :dates)
|> Map.to_list()
|> Enum.map(fn {key, value} -> {Date.to_iso8601(key), value} end)
|> Map.new()
dates =
Map.get(cache, :dates)
|> Map.to_list()
|> Enum.map(fn {key, value} -> {Date.to_iso8601(key), value} end)
|> Map.new()
%{
languages: languages,
......@@ -189,16 +200,19 @@ defmodule CodeStats.User do
# Unformat data from DB to native datatypes
defp unformat_cache_from_db(cache) do
languages = Map.get(cache, "languages")
|> str_keys_to_int()
languages =
Map.get(cache, "languages")
|> str_keys_to_int()
machines = Map.get(cache, "machines")
|> str_keys_to_int()
machines =
Map.get(cache, "machines")
|> str_keys_to_int()
dates = Map.get(cache, "dates")
|> Map.to_list()
|> Enum.map(fn {key, value} -> {Date.from_iso8601!(key), value} end)
|> Map.new()
dates =
Map.get(cache, "dates")
|> Map.to_list()
|> Enum.map(fn {key, value} -> {Date.from_iso8601!(key), value} end)
|> Map.new()
%{
languages: languages,
......@@ -237,6 +251,6 @@ defmodule CodeStats.User do
defp get_caching_duration(start_time) do
Calendar.DateTime.diff(DateTime.utc_now(), start_time)
|> (fn {:ok, s, us, _} -> s + (us / 1_000_000) end).()
|> (fn {:ok, s, us, _} -> s + us / 1_000_000 end).()
end
end
......@@ -4,13 +4,13 @@ defmodule CodeStats.XP do
import Ecto.Changeset
schema "xps" do
field :amount, :integer
belongs_to :pulse, CodeStats.User.Pulse
belongs_to :language, CodeStats.Language
field(:amount, :integer)
belongs_to(:pulse, CodeStats.User.Pulse)
belongs_to(:language, CodeStats.Language)
# Original language can be used to fix alias errors later, it should always use
# the language that was sent. :language field on the other hand follows aliases
belongs_to :original_language, CodeStats.Language
belongs_to(:original_language, CodeStats.Language)
timestamps()
end
......
......@@ -9,7 +9,8 @@ defmodule CodeStats.XP.XPCacheRefresher do
alias CodeStats.{Repo, User}
@how_often 24 * 60 * 60 * 1000 # Run every 24 hours
# Run every 24 hours
@how_often 24 * 60 * 60 * 1000
def start_link do
GenServer.start_link(__MODULE__, %{})
......@@ -32,10 +33,8 @@ defmodule CodeStats.XP.XPCacheRefresher do
end
defp do_refresh() do
(from u in User, select: u)
from(u in User, select: u)
|> Repo.all()
|> Enum.each(
fn user -> User.update_cached_xps(user, true) end
)
|> Enum.each(fn user -> User.update_cached_xps(user, true) end)
end
end
......@@ -8,7 +8,7 @@ defmodule CodeStats.XP.XPCalculator do
@doc """
Get the level based on given XP.
"""
@spec get_level(Integer.t) :: Integer.t
@spec get_level(Integer.t()) :: Integer.t()
def get_level(xp) do
Float.floor(@level_factor * :math.sqrt(xp))
|> trunc
......@@ -17,7 +17,7 @@ defmodule CodeStats.XP.XPCalculator do
@doc """
Get the amount of XP required to reach the next level from the given level.
"""
@spec get_next_level_xp(Integer.t) :: Integer.t