Commit 2052e371 authored by Mikko Ahlroth's avatar Mikko Ahlroth

Add machine stats and some other stuff

* Added machine statistics to profile page
* Added recalculation of XP every 24 hours
* Added favicons for all kinds of platforms
* Added more user info to profile view
parent 5d3804d6
......@@ -13,6 +13,9 @@ defmodule CodeStats do
supervisor(CodeStats.Repo, []),
# Here you could define other workers and supervisors as children
# worker(CodeStats.Worker, [arg1, arg2, arg3]),
# Start XPCacheRefresher
worker(CodeStats.XPCacheRefresher, []),
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
......
defmodule CodeStats.XPCacheRefresher do
@moduledoc """
This module handles refreshing the caches of all users periodically. This is done to avoid
accumulating problems that might happen with miscalculations of CachedXPs.
"""
use GenServer
import Ecto.Query, only: [from: 2]
alias CodeStats.{Repo, User}
@how_often 24 * 60 * 60 * 1000 # Run every 24 hours
def start_link do
GenServer.start_link(__MODULE__, %{})
end
def init(state) do
Process.send_after(self(), :work, @how_often)
{:ok, state}
end
def handle_info(:work, state) do
(from u in User, select: u)
|> Repo.all()
|> Enum.each(
fn user -> User.update_cached_xps(user, true) end
)
# Start the timer again
Process.send_after(self(), :work, @how_often)
{:noreply, state}
end
end
......@@ -3,7 +3,7 @@ defmodule CodeStats.Mixfile do
def project do
[app: :code_stats,
version: "1.2.2",
version: "1.3.0",
elixir: "~> 1.2",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix, :gettext] ++ Mix.compilers,
......
......@@ -11,6 +11,7 @@ defmodule CodeStats.ProfileController do
alias CodeStats.Pulse
alias CodeStats.XP
alias CodeStats.Language
alias CodeStats.Machine
def my_profile(conn, _params) do
user = SetSessionUser.get_user_data(conn)
......@@ -37,43 +38,103 @@ defmodule CodeStats.ProfileController do
end
def render_profile(conn, user) do
now = Calendar.DateTime.now_utc()
latest_xp_since = Calendar.DateTime.subtract!(now, 3600 * 12)
xps = User.update_cached_xps(user)
|> Enum.sort(fn a, b -> a.amount >= b.amount end)
new_xps = get_latest_xps(user)
new_xps = get_latest_xps(user, latest_xp_since)
{machine_xps, new_machine_xps} = get_machine_xps(user, latest_xp_since)
days_coded = get_days_coded(user)
total_xp = Enum.reduce(xps, 0, fn xp, acc -> acc + xp.amount end)
total_new_xp = Enum.reduce(Map.values(new_xps), 0, fn amount, acc -> acc + amount end)
{highlighted_xps, more_xps} = Enum.split(xps, 10)
last_day_coded = Enum.at(days_coded, 0)
xp_per_day = case last_day_coded do
nil -> 0
_ -> Float.round(total_xp / Enum.count(days_coded))
end
conn
|> assign(:user, user)
|> assign(:total_xp, total_xp)
|> assign(:last_day_coded, last_day_coded)
|> assign(:xp_per_day, xp_per_day)
|> assign(:xps, highlighted_xps)
|> assign(:more_xps, more_xps)
|> assign(:new_xps, new_xps)
|> assign(:machine_xps, machine_xps)
|> assign(:new_machine_xps, new_machine_xps)
|> assign(:total_new_xp, total_new_xp)
|> render("profile.html")
end
defp get_latest_xps(user) do
now = Calendar.DateTime.now_utc()
then = Calendar.DateTime.subtract!(now, 3600 * 12)
# Get all XP accumulated in the last 12 hours
defp get_latest_xps(user, then) do
xps_q = from x in XP,
join: p in Pulse, on: p.id == x.pulse_id,
join: l in Language, on: l.id == x.language_id,
where: p.user_id == ^user.id and p.sent_at >= ^then,
select: {x.amount, l.name}
case Repo.all(xps_q) do
nil -> %{}
ret ->
Enum.reduce(ret, %{}, fn {xp, language}, acc ->
amount = Map.get(acc, language, 0) + xp
Map.put(acc, language, amount)
end)
end
end
# Get all XP per machine and XP per machine per last 12 hours
defp get_machine_xps(user, then) do
xps_q = from m in Machine,
join: p in Pulse, on: m.id == p.machine_id,
join: x in XP, on: p.id == x.pulse_id,
where: m.user_id == ^user.id,
group_by: m.id,
order_by: [desc: sum(x.amount)],
select: {m, sum(x.amount)}
xps = case Repo.all(xps_q) do
nil -> []
ret -> ret
end
Enum.reduce(xps, %{}, fn {xp, language}, acc ->
amount = Map.get(acc, language, 0) + xp
Map.put(acc, language, amount)
end)
new_xps_q = from m in Machine,
join: p in Pulse, on: m.id == p.machine_id,
join: x in XP, on: p.id == x.pulse_id,
where: m.user_id == ^user.id and p.sent_at >= ^then,
group_by: m.id,
order_by: [desc: sum(x.amount)],
select: {m, sum(x.amount)}
new_xps = case Repo.all(new_xps_q) do
nil -> %{}
ret ->
Enum.reduce(ret, %{}, fn {machine, amount}, acc ->
Map.put(acc, machine.id, {machine, amount})
end)
end
{xps, new_xps}
end
# Get amount of days when user has coded at least something
defp get_days_coded(user) do
days_q = from p in Pulse,
where: p.user_id == ^user.id,
group_by: fragment("DATE(?)", p.sent_at),
select: fragment("DATE(?)", p.sent_at),
order_by: [desc: fragment("DATE(?)", p.sent_at)]
case Repo.all(days_q) do
nil -> []
ret -> ret
end
end
end
......@@ -77,9 +77,12 @@ defmodule CodeStats.User do
Will first load all existing CachedXP, then sum any new XPs per Language and add those to
the CachedXPs, creating new ones if required.
If `update_all` is set, all XP is gathered and CachedXP is replaced, not just added to. This
results in a total recalculation of all the user's XP.
"""
def update_cached_xps(user) do
last_cached = if user.last_cached != nil do
def update_cached_xps(user, update_all \\ false) do
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)
......@@ -97,14 +100,21 @@ defmodule CodeStats.User do
|> Enum.reduce(%{}, fn cached_xp, acc ->
# Each cached XP is inserted into a 3-tuple of {CachedXP, dirty bit, amount}
# The dirty bit is used to persist only changed CachedXPs
Map.put(acc, cached_xp.language_id, {cached_xp, false, cached_xp.amount})
# If we are updating all XP, every CachedXP will be marked as dirty and will have
# amount set to 0 to start with
if update_all do
Map.put(acc, cached_xp.language_id, {cached_xp, true, 0})
else
Map.put(acc, cached_xp.language_id, {cached_xp, false, cached_xp.amount})
end
end)
# Load all of user's XPs plus the Language for each XP
# Load all of user's new XP plus the Language for each XP
xps_q = from x in XP,
join: p in Pulse, on: p.id == x.pulse_id,
join: l in Language, on: l.id == x.language_id,
where: p.user_id == ^user.id and p.sent_at >= ^last_cached,
where: p.user_id == ^user.id and p.inserted_at >= ^last_cached,
select: {x, l}
xps = case Repo.all(xps_q) do
......
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="/mstile-70x70.png"/>
<square150x150logo src="/mstile-150x150.png"/>
<square310x310logo src="/mstile-310x310.png"/>
<wide310x150logo src="/mstile-310x150.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>
web/static/assets/favicon.ico

1.23 KB | W: | H:

web/static/assets/favicon.ico

7.23 KB | W: | H:

web/static/assets/favicon.ico
web/static/assets/favicon.ico
web/static/assets/favicon.ico
web/static/assets/favicon.ico
  • 2-up
  • Swipe
  • Onion skin
{
"name": "Code::Stats",
"icons": [
{
"src": "\/android-chrome-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": 0.75
},
{
"src": "\/android-chrome-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": 1
},
{
"src": "\/android-chrome-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": 1.5
},
{
"src": "\/android-chrome-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": 2
},
{
"src": "\/android-chrome-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": 3
},
{
"src": "\/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": 4
}
]
}
......@@ -3,3 +3,6 @@
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /
User-agent: *
Disallow: /api
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="610.000000pt" height="610.000000pt" viewBox="0 0 610.000000 610.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,610.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1770 4834 c-406 -56 -763 -228 -1030 -494 -260 -261 -427 -598 -500
-1010 -28 -158 -38 -500 -21 -676 34 -341 132 -634 292 -875 83 -125 263 -305
389 -388 231 -154 474 -243 795 -292 186 -29 556 -32 745 -6 134 19 296 54
398 88 l62 21 0 194 c0 107 -2 194 -4 194 -2 0 -64 -15 -137 -34 -380 -97
-733 -112 -1057 -46 -575 119 -926 512 -1007 1127 -17 133 -20 430 -4 553 32
261 116 496 247 695 60 91 197 233 293 305 301 224 742 315 1194 244 102 -16
301 -61 388 -88 l47 -15 0 198 0 197 -47 17 c-87 30 -197 57 -318 78 -160 28
-567 36 -725 13z"/>
<path d="M4525 4844 c-488 -68 -781 -251 -941 -587 -129 -273 -124 -694 13
-926 51 -85 169 -210 251 -265 177 -117 294 -159 802 -286 376 -95 507 -149
619 -254 108 -103 149 -192 158 -346 21 -370 -191 -608 -612 -685 -140 -26
-423 -31 -585 -11 -231 29 -450 83 -618 152 -48 19 -95 38 -104 41 -17 5 -18
-9 -18 -196 l0 -201 48 -24 c121 -61 343 -121 582 -157 190 -29 524 -32 703
-5 597 87 941 358 1043 822 24 108 24 379 0 483 -37 166 -97 272 -226 401
-182 182 -310 237 -901 390 -418 109 -540 160 -655 274 -133 133 -174 320
-118 541 122 477 824 593 1669 276 l100 -38 3 200 c1 111 -2 205 -7 210 -14
14 -252 94 -362 122 -214 54 -318 67 -574 70 -132 2 -253 1 -270 -1z"/>
<path d="M1709 3657 c-123 -46 -175 -175 -116 -288 73 -137 281 -137 354 0 49
93 19 210 -68 263 -53 34 -121 43 -170 25z"/>
<path d="M2599 3657 c-123 -46 -175 -175 -116 -288 73 -137 281 -137 354 0 49
93 19 210 -68 263 -53 34 -121 43 -170 25z"/>
<path d="M1705 2678 c-108 -39 -165 -168 -121 -271 17 -39 71 -92 109 -106 39
-15 121 -14 160 2 45 19 105 89 113 132 24 131 -58 243 -185 251 -25 2 -59 -1
-76 -8z"/>
<path d="M2595 2678 c-108 -39 -165 -168 -121 -271 17 -39 71 -92 109 -106 39
-15 121 -14 160 2 45 19 105 89 113 132 24 131 -58 243 -185 251 -25 2 -59 -1
-76 -8z"/>
</g>
</svg>
......@@ -83,3 +83,17 @@ form.link, form.button {
.machine-action {
margin-right: 5px;
}
.profile-detail-list {
list-style: none;
margin-left: 0;
padding-left: 0;
}
.profile-detail-list li {
/*display: inline;*/
}
.profile-detail-list li:before {
content: "☞ ";
}
......@@ -4,11 +4,30 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<meta name="description" content="Code::Stats is a free stats tracking service for programmers">
<meta name="author" content="Mikko Ahlroth">
<title><%= get_conf(:site_name) %></title>
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png">
<link rel="icon" type="image/png" href="/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/android-chrome-192x192.png" sizes="192x192">
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
<meta name="theme-color" content="#ffffff">
</head>
<body>
......
......@@ -24,6 +24,17 @@
<h2>Changelog</h2>
<h3>1.3.0 – 2016-06-04 – Machine stats</h3>
<p>
<ul>
<li>Added machine statistics to profile page.</li>
<li>Added recalculation of XP once a day to fix XP errors in profile view.</li>
<li>Added favicons.</li>
<li>Added more user info to profile view.</li>
</ul>
</p>
<h3>1.2.2 – 2016-06-02 – Vincit Oy</h3>
<p>
......
<div class="row">
<div class="col-xs-12 col-sm-6">
<div class="col-xs-12 col-sm-7">
<h3>
Level
<%= get_level(@total_xp) %>
......@@ -19,10 +19,21 @@
</div>
</div>
<div class="col-xs-12 col-sm-6">
<div class="col-xs-12 col-sm-5">
<h2><%= @user.username %></h2>
<p>
Programming since <%= Calendar.Strftime.strftime!(@user.inserted_at, "%F") %>.
<ul class="profile-detail-list">
<li>Programming since <%= Calendar.Strftime.strftime!(@user.inserted_at, "%F") %>.</li>
<li>Average <%= @xp_per_day %>&nbsp;XP per day.</li>
<li>
Last coded
<%= if @last_day_coded != nil do %>
at <%= Calendar.Strftime.strftime!(@last_day_coded, "%F") %>.
<% else %>
<em>never</em>.
<% end %>
</li>
</ul>
</p>
</div>
</div>
......@@ -88,22 +99,22 @@
</div>
<% end %>
<%= if not Enum.empty?(@more_xps) do %>
<div class="row">
<div class="col-xs-12">
<hr />
</div>
<div class="row">
<div class="col-xs-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="row">
<%= if not Enum.empty?(@more_xps) do %>
<div class="col-xs-12 col-sm-6">
<h4>Other languages</h4>
<ol start="11">
<%= for xp <- @more_xps do %>
<% new_xp = Map.get(@new_xps, xp.language.name, 0) %>
<li>
<%= xp.language.name %>
<strong><%= xp.language.name %></strong>
level
<%= get_level(xp.amount) %>
(<%= format_xp(xp.amount) %>&nbsp;XP)
......@@ -117,5 +128,38 @@
<% end %>
</ol>
</div>
</div>
<% end %>
<% end %>
<%= if not Enum.empty?(@machine_xps) do %>
<div class="col-xs-12 col-sm-6">
<h4>Machines</h4>
<ol>
<%= for {machine, amount} <- @machine_xps do %>
<% new_xp = Map.get(@new_machine_xps, machine.id, 0) %>
<li>
<strong>
<%= machine.name %>
level
<%= get_level(amount) %>
(<%= format_xp(amount) %>&nbsp;XP)
</strong>
<%= if new_xp > 0 do %>
<sup>
(+<%= format_xp(new_xp) %>)
</sup>
<% end %>
<div class="progress">
<% {old_width, new_width} = get_xp_bar_widths(amount, new_xp) %>
<div class="progress-bar progress-bar-success" role="progressbar" style="width: <%= old_width %>%"></div>
<div class="progress-bar progress-bar-striped progress-bar-warning" role="progressbar" style="width: <%= new_width %>%"></div>
</div>
</li>
<% end %>
</ol>
</div>
<% end %>
</div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment