├── app.json
├── web
├── static
│ ├── js
│ │ ├── components
│ │ │ ├── util
│ │ │ │ ├── index.js
│ │ │ │ └── raw.js
│ │ │ ├── index.js
│ │ │ ├── selectable
│ │ │ │ ├── selectable.styl
│ │ │ │ └── index.js
│ │ │ ├── project-token
│ │ │ │ ├── project-token.styl
│ │ │ │ ├── project-token.jade
│ │ │ │ └── index.js
│ │ │ ├── click-safe.js
│ │ │ ├── badges
│ │ │ │ ├── badges.styl
│ │ │ │ ├── index.js
│ │ │ │ └── badges.jade
│ │ │ └── file-coverage
│ │ │ │ ├── index.js
│ │ │ │ ├── file-coverage.jade
│ │ │ │ └── file-coverage.styl
│ │ └── app.js
│ └── css
│ │ ├── main.styl
│ │ ├── pages
│ │ ├── user.styl
│ │ ├── files.styl
│ │ ├── build.styl
│ │ ├── admin.styl
│ │ └── project.styl
│ │ ├── partials
│ │ ├── base.styl
│ │ ├── breadcrumb.styl
│ │ ├── global.styl
│ │ ├── util.styl
│ │ └── header.styl
│ │ └── theme.less
├── views
│ ├── user_view.ex
│ ├── profile_view.ex
│ ├── admin
│ │ ├── settings_view.ex
│ │ ├── shared_view.ex
│ │ ├── dashboard_view.ex
│ │ ├── project_view.ex
│ │ └── user_view.ex
│ ├── build_view.ex
│ ├── shared_view.ex
│ ├── layout_view.ex
│ ├── job_view.ex
│ ├── api
│ │ └── v1
│ │ │ └── job_view.ex
│ ├── project_view.ex
│ ├── auth_view.ex
│ ├── error_view.ex
│ ├── file_view.ex
│ ├── form_helpers.ex
│ ├── common_view.ex
│ └── error_helpers.ex
├── templates
│ ├── mailers
│ │ └── user
│ │ │ ├── reset_password.text.eex
│ │ │ ├── reset_password.html.eex
│ │ │ ├── confirmation.text.eex
│ │ │ └── confirmation.html.eex
│ ├── build
│ │ ├── commit.html.eex
│ │ └── show.html.eex
│ ├── shared
│ │ └── coverage_diff.html.eex
│ ├── user
│ │ ├── new.html.eex
│ │ └── form.html.eex
│ ├── project
│ │ ├── new.html.eex
│ │ ├── form.html.eex
│ │ ├── edit.html.eex
│ │ ├── index.html.eex
│ │ └── show.html.eex
│ ├── profile
│ │ ├── edit.html.eex
│ │ ├── edit_password.html.eex
│ │ ├── reset_password.html.eex
│ │ ├── reset_password_request.html.eex
│ │ └── password_form.html.eex
│ ├── admin
│ │ ├── shared
│ │ │ └── actions.html.eex
│ │ ├── user
│ │ │ ├── new.html.eex
│ │ │ ├── edit.html.eex
│ │ │ ├── show.html.eex
│ │ │ └── index.html.eex
│ │ ├── project
│ │ │ ├── show.html.eex
│ │ │ └── index.html.eex
│ │ ├── settings
│ │ │ └── edit.html.eex
│ │ └── dashboard
│ │ │ └── index.html.eex
│ ├── file
│ │ ├── show.html.eex
│ │ └── list.html.eex
│ ├── auth
│ │ └── login.html.eex
│ ├── job
│ │ └── show.html.eex
│ └── layout
│ │ ├── app.html.eex
│ │ └── header.html.eex
├── models
│ ├── settings.ex
│ ├── project.ex
│ ├── user.ex
│ ├── badge.ex
│ ├── job.ex
│ ├── file.ex
│ └── build.ex
├── controllers
│ ├── file_controller.ex
│ ├── job_controller.ex
│ ├── admin
│ │ ├── dashboard_controller.ex
│ │ ├── project_controller.ex
│ │ ├── settings_controller.ex
│ │ └── user_controller.ex
│ ├── build_controller.ex
│ ├── api
│ │ └── v1
│ │ │ └── job_controller.ex
│ ├── auth_controller.ex
│ ├── user_controller.ex
│ ├── project_controller.ex
│ └── profile_controller.ex
├── gettext.ex
├── services
│ ├── file_service.ex
│ └── user_service.ex
├── mailers
│ ├── app_mailer.ex
│ └── user_mailer.ex
├── managers
│ ├── settings_manager.ex
│ ├── badge_manager.ex
│ ├── file_manager.ex
│ ├── project_manager.ex
│ ├── job_manager.ex
│ ├── build_manager.ex
│ └── user_manager.ex
├── web.ex
└── router.ex
├── compile
├── priv
├── static
│ ├── favicon.ico
│ ├── robots.txt
│ └── images
│ │ └── logo.svg
├── gettext
│ └── en
│ │ └── LC_MESSAGES
│ │ ├── project.po
│ │ ├── settings.po
│ │ ├── user.po
│ │ └── errors.po
└── repo
│ ├── seeds
│ ├── 0001_settings.exs
│ ├── eyecatch
│ │ ├── 0010_projects.exs
│ │ ├── 0012_jobs.exs
│ │ ├── 0011_builds.exs
│ │ └── 0013_files.exs
│ └── 0002_admin.exs
│ └── migrations
│ ├── 20151213015301_create_settings.exs
│ ├── 20151108002255_create_badge.exs
│ ├── 20151022125349_create_project.exs
│ ├── 20151022144345_create_file.exs
│ ├── 20151022143636_create_job.exs
│ ├── 20151202133406_create_user.exs
│ └── 20151022141210_create_build.exs
├── test
├── models
│ ├── user_test.exs
│ ├── settings_test.exs
│ ├── project_test.exs
│ ├── job_test.exs
│ ├── build_test.exs
│ ├── badge_test.exs
│ └── file_test.exs
├── views
│ ├── layout_view_test.exs
│ ├── job_view_test.exs
│ ├── file_view_test.exs
│ ├── error_view_test.exs
│ └── common_view_test.exs
├── test_helper.exs
├── support
│ ├── ecto_with_changeset_stragegy.ex
│ ├── fixtures.ex
│ ├── manager_case.ex
│ ├── channel_case.ex
│ ├── conn_case.ex
│ ├── model_case.ex
│ └── factory.ex
├── lib
│ ├── types
│ │ └── json_test.exs
│ └── badge_creator_test.exs
├── controllers
│ ├── file_controller_test.exs
│ ├── build_controller_test.exs
│ ├── job_controller_test.exs
│ ├── api
│ │ └── v1
│ │ │ └── job_controller_test.exs
│ └── project_controller_test.exs
└── managers
│ ├── settings_manager_test.exs
│ ├── user_manager_test.exs
│ ├── badge_manager_test.exs
│ ├── file_manager_test.exs
│ ├── project_manager_test.exs
│ ├── job_manager_test.exs
│ └── build_manager_test.exs
├── .dockerignore
├── lib
├── opencov
│ ├── imagemagick.ex
│ ├── types
│ │ └── json.ex
│ ├── helpers
│ │ ├── authentication.ex
│ │ ├── datetime.ex
│ │ └── display.ex
│ ├── authentication.ex
│ ├── plug
│ │ ├── anonymous_only.ex
│ │ ├── fetch_user.ex
│ │ ├── authentication.ex
│ │ └── force_password_initialize.ex
│ ├── core.ex
│ ├── repo.ex
│ ├── endpoint.ex
│ ├── templates
│ │ └── badge_template.eex
│ ├── mailer.ex
│ └── badge_creator.ex
└── opencov.ex
├── elixir_buildpack.config
├── phoenix_static_buildpack.config
├── .gitignore
├── .travis.yml
├── cover.sh
├── Dockerfile
├── docker-compose.yml
├── config
├── local.sample.exs
├── test.exs
├── prod.exs
├── config.exs
└── dev.exs
├── .eslintrc
├── LICENSE
├── eyecatch.rb
├── package.json
├── mix.exs
├── webpack.config.js
└── README.md
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "stack": "heroku-20"
3 | }
4 |
--------------------------------------------------------------------------------
/web/static/js/components/util/index.js:
--------------------------------------------------------------------------------
1 | import './raw'
2 |
--------------------------------------------------------------------------------
/compile:
--------------------------------------------------------------------------------
1 | ./node_modules/.bin/webpack -p
2 | mix phx.digest
3 |
--------------------------------------------------------------------------------
/web/views/user_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.UserView do
2 | use Opencov.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danhper/opencov/HEAD/priv/static/favicon.ico
--------------------------------------------------------------------------------
/test/models/user_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.UserTest do
2 | use Opencov.ModelCase
3 | end
4 |
--------------------------------------------------------------------------------
/web/static/css/main.styl:
--------------------------------------------------------------------------------
1 | @import "nib"
2 |
3 | @import "./partials/*"
4 | @import "./pages/*"
5 |
--------------------------------------------------------------------------------
/test/models/settings_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.SettingsTest do
2 | use Opencov.ModelCase
3 | end
4 |
--------------------------------------------------------------------------------
/web/views/profile_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ProfileView do
2 | use Opencov.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | _build
2 | deps
3 | node_modules
4 | .git
5 | .tags
6 | postgresql-data
7 | priv/static/js
8 |
--------------------------------------------------------------------------------
/test/models/project_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ProjectTest do
2 | use Opencov.ModelCase
3 |
4 | end
5 |
--------------------------------------------------------------------------------
/web/views/admin/settings_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Admin.SettingsView do
2 | use Opencov.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/web/views/admin/shared_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Admin.SharedView do
2 | use Opencov.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/project.po:
--------------------------------------------------------------------------------
1 | msgid "name"
2 | msgstr "Name"
3 |
4 | msgid "base_url"
5 | msgstr "URL"
6 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/settings.po:
--------------------------------------------------------------------------------
1 | msgid "default_project_visibility"
2 | msgstr "Default project visibility"
3 |
--------------------------------------------------------------------------------
/web/static/css/pages/user.styl:
--------------------------------------------------------------------------------
1 | .user-form
2 | .admin.form-group
3 | margin 2em 0
4 | user-select none
5 |
--------------------------------------------------------------------------------
/test/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.LayoutViewTest do
2 | use Opencov.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/web/static/css/pages/files.styl:
--------------------------------------------------------------------------------
1 | .files
2 | table
3 | th, td
4 | &:first-child, &:nth-child(2)
5 | width 120px
6 |
--------------------------------------------------------------------------------
/web/views/build_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BuildView do
2 | use Opencov.Web, :view
3 |
4 | import Opencov.CommonView
5 | end
6 |
--------------------------------------------------------------------------------
/web/views/shared_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.SharedView do
2 | use Opencov.Web, :view
3 |
4 | import Opencov.CommonView
5 | end
6 |
--------------------------------------------------------------------------------
/web/views/admin/dashboard_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Admin.DashboardView do
2 | use Opencov.Web, :view
3 |
4 | alias Opencov.Helpers.Display
5 | end
6 |
--------------------------------------------------------------------------------
/web/static/js/components/util/raw.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 |
3 | riot.tag('raw', ' ', function (opts) {
4 | this.root.innerHTML = opts.html
5 | })
6 |
--------------------------------------------------------------------------------
/web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.LayoutView do
2 | use Opencov.Web, :view
3 |
4 | import Opencov.Helpers.Authentication
5 | import Exgravatar
6 | end
7 |
--------------------------------------------------------------------------------
/web/static/css/partials/base.styl:
--------------------------------------------------------------------------------
1 | .body-wrapper
2 | padding 0
3 |
4 | main.container
5 | padding 1em
6 | background #fff
7 | min-height calc(100vh - 52px)
8 |
--------------------------------------------------------------------------------
/web/static/css/partials/breadcrumb.styl:
--------------------------------------------------------------------------------
1 | .breadcrumb
2 | background-color inherit
3 | padding 0.5em 1.5em
4 | margin-bottom 0
5 |
6 | .media-body
7 | line-height 80px
8 |
--------------------------------------------------------------------------------
/web/static/js/components/index.js:
--------------------------------------------------------------------------------
1 | import './file-coverage'
2 | import './project-token'
3 | import './selectable'
4 | import './badges'
5 | import './click-safe'
6 | import './util'
7 |
--------------------------------------------------------------------------------
/web/static/js/components/selectable/selectable.styl:
--------------------------------------------------------------------------------
1 | selectable
2 | textarea
3 | width 100%
4 | resize none
5 | overflow hidden
6 | border none
7 | cursor pointer
8 |
--------------------------------------------------------------------------------
/web/views/admin/project_view.ex:
--------------------------------------------------------------------------------
1 |
2 | defmodule Opencov.Admin.ProjectView do
3 | use Opencov.Web, :view
4 |
5 | import Scrivener.HTML
6 | alias Opencov.Helpers.Datetime
7 | end
8 |
--------------------------------------------------------------------------------
/web/views/admin/user_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Admin.UserView do
2 | use Opencov.Web, :view
3 |
4 | import Scrivener.HTML
5 | alias Opencov.Helpers.Display
6 | alias Opencov.Helpers.Datetime
7 | end
8 |
--------------------------------------------------------------------------------
/web/templates/mailers/user/reset_password.text.eex:
--------------------------------------------------------------------------------
1 | Hi <%= user.name %>,
2 |
3 | Thank you for using Opencov.
4 |
5 | Please visit the following address to reset your password.
6 |
7 | <%= reset_password_url %>
8 |
--------------------------------------------------------------------------------
/web/views/job_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.JobView do
2 | use Opencov.Web, :view
3 |
4 | import Opencov.CommonView
5 |
6 | def job_time(job) do
7 | job.run_at || job.inserted_at
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/opencov/imagemagick.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ImageMagick do
2 | def convert(args) do
3 | command = Application.get_env(:opencov, :imagemagick_convert_path) || "convert"
4 | System.cmd(command, args)
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/web/static/css/partials/global.styl:
--------------------------------------------------------------------------------
1 | a
2 | &:active, &:hover
3 | text-decoration none
4 | h2
5 | padding 0 1em
6 |
7 | form.full-page
8 | padding 20px 10%
9 |
10 |
11 | table td > form
12 | display inline
13 |
--------------------------------------------------------------------------------
/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/elixir_buildpack.config:
--------------------------------------------------------------------------------
1 | erlang_version=20.3
2 | elixir_version=1.6
3 | always_rebuild=false
4 | config_vars_to_export=(DATABASE_URL OPENCOV_AUTH OPENCOV_USER OPENCOV_PASSWORD SECRET_KEY_BASE OPENCOV_SCHEME OPENCOV_HOST OPENCOV_PORT POSTGRES_POOL_SIZE OPENCOV_DEMO)
5 |
--------------------------------------------------------------------------------
/phoenix_static_buildpack.config:
--------------------------------------------------------------------------------
1 | node_version=4.8.7
2 | npm_version=4.0.5
3 | config_vars_to_export=(DATABASE_URL OPENCOV_AUTH OPENCOV_USER OPENCOV_PASSWORD SECRET_KEY_BASE OPENCOV_SCHEME OPENCOV_HOST OPENCOV_PORT POSTGRES_POOL_SIZE OPENCOV_DEMO)
4 | compile="compile"
5 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start
2 |
3 | Application.ensure_all_started(:ex_machina)
4 |
5 | Mix.Task.run "ecto.create", ["--quiet"]
6 | Mix.Task.run "ecto.migrate", ["--quiet"]
7 | Mix.Task.run "seedex.seed"
8 | Ecto.Adapters.SQL.Sandbox.mode(Opencov.Repo, :manual)
9 |
--------------------------------------------------------------------------------
/web/static/css/pages/build.styl:
--------------------------------------------------------------------------------
1 | .single-build
2 | .breadcrumb
3 | .build-number
4 | font-size 0.9em
5 |
6 | .jobs
7 | th, td
8 | &:first-child
9 | width 50px
10 | &:nth-child(2), &:nth-child(3)
11 | width 120px
12 |
--------------------------------------------------------------------------------
/web/static/js/components/project-token/project-token.styl:
--------------------------------------------------------------------------------
1 | @import 'nib'
2 |
3 | project-token
4 | user-select none
5 | width 100%
6 | display block
7 | .fa
8 | margin-right 0.3em
9 | .token
10 | font-size 0.5em
11 | border-radius 2px
12 |
--------------------------------------------------------------------------------
/web/views/api/v1/job_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Api.V1.JobView do
2 | use Opencov.Web, :view
3 |
4 | @attributes ~w(id project_id build_id coverage)a
5 |
6 | def render("show.json", %{job: job}) do
7 | job |> Map.take(@attributes)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/web/static/js/app.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 |
3 | import 'phoenix_html'
4 |
5 | import '../css/main.styl'
6 |
7 | import './components'
8 |
9 | riot.mount('*')
10 |
11 | // Fix logout link
12 | $(document).off('click.bs.dropdown.data-api', '.dropdown form')
13 |
--------------------------------------------------------------------------------
/web/static/js/components/click-safe.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 |
3 | const template = '
'
4 |
5 | riot.tag('click-safe', template, function (opts) {
6 | this.preventClick = (e) => {
7 | e.stopPropagation()
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/priv/repo/seeds/0001_settings.exs:
--------------------------------------------------------------------------------
1 | import Seedex
2 |
3 | seed Opencov.Settings, fn settings ->
4 | settings
5 | |> Map.put(:id, 1)
6 | |> Map.put(:signup_enabled, false)
7 | |> Map.put(:restricted_signup_domains, "")
8 | |> Map.put(:default_project_visibility, "internal")
9 | end
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/phoenix
2 |
3 | ### phoenix ###
4 | # Phoenix: a web framework for Elixir
5 | _build/
6 | deps/
7 | node_modules/
8 | priv/static/js
9 | config/prod.secret.exs
10 | config/local.exs
11 | *.coverdata
12 |
13 | *.dump
14 |
15 | postgresql-data
16 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/user.po:
--------------------------------------------------------------------------------
1 | msgid "name"
2 | msgstr "Name"
3 |
4 | msgid "email"
5 | msgstr "Email"
6 |
7 | msgid "password"
8 | msgstr "Password"
9 |
10 | msgid "password_confirmation"
11 | msgstr "Password confirmation"
12 |
13 | msgid "current_password"
14 | msgstr "Current password"
15 |
--------------------------------------------------------------------------------
/test/support/ecto_with_changeset_stragegy.ex:
--------------------------------------------------------------------------------
1 | defmodule ExMachina.EctoWithChangesetStrategy do
2 | use ExMachina.Strategy, function_name: :insert
3 |
4 | def handle_insert(record, %{repo: repo, factory_module: module}) do
5 | record
6 | |> module.make_changeset
7 | |> repo.insert!
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/opencov/types/json.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Types.JSON do
2 | @behaviour Ecto.Type
3 |
4 | def type, do: :json
5 |
6 | def cast(any), do: {:ok, any}
7 | def load(value), do: Jason.decode(value)
8 | def dump(value) when is_binary(value), do: {:ok, value}
9 | def dump(value), do: Jason.encode(value)
10 | end
11 |
--------------------------------------------------------------------------------
/web/templates/build/commit.html.eex:
--------------------------------------------------------------------------------
1 | <%= if @build.commit_message do %>
2 | <%= @build.commit_message %>
3 | <%= if @build.commit_sha do %>
4 | <%= link to: commit_link(@build.project, @build.commit_sha) do %>
5 |
6 | <% end %>
7 | <% end %>
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/web/templates/shared/coverage_diff.html.eex:
--------------------------------------------------------------------------------
1 | <%= if abs(@diff) >= 0.001 do %>
2 | ">
3 | fa-small"> <%= format_coverage(abs(@diff)) %>
4 |
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/web/models/settings.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Settings do
2 | use Opencov.Web, :model
3 |
4 | schema "settings" do
5 | field :signup_enabled, :boolean, default: false
6 | field :restricted_signup_domains, :string, default: ""
7 | field :default_project_visibility, :string
8 |
9 | timestamps()
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/web/static/js/components/badges/badges.styl:
--------------------------------------------------------------------------------
1 | badges
2 | .badge-list
3 | padding 0.5em 1em
4 | margin-bottom 0
5 | dd
6 | margin-bottom 1em
7 | &:last-child
8 | margin-bottom 0
9 |
10 | .formats
11 | margin 0 auto
12 | width 50%
13 | & > li
14 | & > a
15 | padding 5px 10px
16 |
--------------------------------------------------------------------------------
/web/static/js/components/project-token/project-token.jade:
--------------------------------------------------------------------------------
1 | a(onclick="{ toggleShown }" href="javascript:void(0)")
2 | i(class="{ fa: true, 'fa-eye': !shown, 'fa-eye-slash': shown }")
3 | input.token(if="{ shown }" value="{ token }" readonly riot-style="width: { computeWidth() }px" name="token-input")
4 | span(if="{ !shown }") Show token
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 |
3 | elixir:
4 | - 1.4
5 | - 1.5
6 |
7 | services:
8 | - postgresql
9 |
10 | otp_release:
11 | - 19.3
12 |
13 | before_script:
14 | - mix compile
15 | - mix ecto.create
16 | - mix ecto.migrate
17 |
18 | script:
19 | - ./cover.sh
20 |
21 | env:
22 | global:
23 | - MIX_ENV=test
24 |
--------------------------------------------------------------------------------
/cover.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # wake up heroku!
4 | curl http://demo.opencov.com || true
5 |
6 | MIX_ENV=test mix coveralls.post \
7 | --sha="$TRAVIS_COMMIT" \
8 | --committer="$(git log -1 $TRAVIS_COMMIT --pretty=format:'%cN')" \
9 | --message="$(git log -1 $TRAVIS_COMMIT --pretty=format:'%s')" \
10 | --branch="$TRAVIS_BRANCH"
11 |
--------------------------------------------------------------------------------
/test/support/fixtures.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Fixtures do
2 | @coverages_path Path.join(__DIR__, "../fixtures/dummy-coverages.json")
3 | @coverages @coverages_path |> File.read! |> Jason.decode!
4 |
5 | def dummy_coverages do
6 | @coverages
7 | end
8 |
9 | def dummy_coverage do
10 | Enum.at(dummy_coverages(), 0)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/test/support/manager_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ManagerCase do
2 | use ExUnit.CaseTemplate
3 |
4 | using do
5 | quote do
6 | alias Opencov.Repo
7 | import Opencov.Factory
8 | import Opencov.ManagerCase
9 | end
10 | end
11 |
12 | setup _tags do
13 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Opencov.Repo)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/opencov/helpers/authentication.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Helpers.Authentication do
2 | def demo?() do
3 | !!Application.get_env(:opencov, :demo)[:enabled]
4 | end
5 |
6 | def current_user(conn) do
7 | Map.fetch!(conn.assigns, :current_user)
8 | end
9 |
10 | def user_signed_in?(conn) do
11 | Map.fetch(conn.assigns, :current_user) != :error
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/priv/repo/seeds/eyecatch/0010_projects.exs:
--------------------------------------------------------------------------------
1 | import Seedex
2 |
3 | seed Opencov.Project, [:name], fn project ->
4 | project
5 | |> Map.put(:id, 1)
6 | |> Map.put(:name, "tuvistavie/opencov")
7 | |> Map.put(:base_url, "https://github.com/tuvistavie/opencov")
8 | |> Map.put(:current_coverage, 72.83)
9 | |> Map.put(:user_id, 1)
10 | |> Map.put(:token, "very-secure-token")
11 | end
12 |
--------------------------------------------------------------------------------
/web/controllers/file_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.FileController do
2 | use Opencov.Web, :controller
3 |
4 | alias Opencov.File
5 |
6 | def show(conn, %{"id" => id}) do
7 | file = Repo.get!(File, id) |> Opencov.Repo.preload(job: [build: :project])
8 | file_json = Jason.encode!(file)
9 | render(conn, "show.html", file: file, file_json: file_json)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/web/static/js/components/selectable/index.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 |
3 | import './selectable.styl'
4 |
5 | riot.tag('selectable',
6 | '',
8 | function (opts) {
9 | this.selectText = (e) => {
10 | e.stopPropagation()
11 | this.root.children[0].select()
12 | }
13 | }
14 | )
15 |
--------------------------------------------------------------------------------
/lib/opencov/helpers/datetime.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Helpers.Datetime do
2 | def format(datetime, :short) do
3 | Timex.format!(datetime, "%Y/%m/%d %H:%M", :strftime)
4 | end
5 | def format(datetime, :dateonly) do
6 | Timex.format!(datetime, "%Y/%m/%d", :strftime)
7 | end
8 | def format(datetime, _), do: format(datetime)
9 | def format(datetime), do: Timex.format!(datetime, "{ISOz}")
10 | end
11 |
--------------------------------------------------------------------------------
/web/templates/user/new.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Sign up
5 |
6 |
7 |
8 |
9 | <%= render "form.html", changeset: @changeset,
10 | action: user_path(@conn, :create),
11 | show_password: true,
12 | is_admin: false %>
13 |
14 |
--------------------------------------------------------------------------------
/lib/opencov/authentication.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Authentication do
2 | import Plug.Conn, only: [put_session: 3, delete_session: 2]
3 |
4 | @user_id_key :user_id
5 |
6 | def login(conn, user) do
7 | put_session(conn, user_id_key(), user.id)
8 | end
9 |
10 | def logout(conn) do
11 | delete_session(conn, user_id_key())
12 | end
13 |
14 | def user_id_key() do
15 | @user_id_key
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM elixir:1.12-alpine
2 |
3 | RUN apk add --update-cache build-base git postgresql-client nodejs yarn
4 |
5 | WORKDIR /opencov
6 |
7 | ENV MIX_ENV prod
8 |
9 | RUN mix local.hex --force && mix local.rebar --force
10 |
11 | COPY mix.exs mix.lock package.json yarn.lock ./
12 |
13 | RUN yarn install && mix deps.get
14 |
15 | COPY . .
16 |
17 | RUN mix compile && mix assets.compile
18 |
19 | CMD ["mix", "phx.server"]
20 |
--------------------------------------------------------------------------------
/priv/repo/seeds/0002_admin.exs:
--------------------------------------------------------------------------------
1 | import Seedex
2 |
3 | # password: p4ssw0rd
4 | seed Opencov.User, [:email], fn user ->
5 | user
6 | |> Map.put(:email, "admin@example.com")
7 | |> Map.put(:password_digest, "$2b$12$hlXtMVOFfd2PxsmyDCEmwuPlHk8M1kOpOBozLSj5GO1Tn6COpHKG.")
8 | |> Map.put(:password_initialized, true)
9 | |> Map.put(:confirmed_at, Timex.now)
10 | |> Map.put(:name, "Admin")
11 | |> Map.put(:admin, true)
12 | end
13 |
--------------------------------------------------------------------------------
/web/controllers/job_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.JobController do
2 | use Opencov.Web, :controller
3 |
4 | alias Opencov.Job
5 | alias Opencov.FileService
6 |
7 | def show(conn, %{"id" => id} = params) do
8 | job = Repo.get!(Job, id) |> Opencov.Repo.preload(build: :project)
9 | file_params = FileService.files_with_filter(job, params)
10 | render(conn, "show.html", [{:job, job}|file_params])
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/web/controllers/admin/dashboard_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Admin.DashboardController do
2 | use Opencov.Web, :controller
3 |
4 | alias Opencov.Repo
5 |
6 | def index(conn, _params) do
7 | users = Repo.latest(Opencov.User)
8 | projects = Repo.latest(Opencov.Project)
9 | settings = Opencov.SettingsManager.get!
10 | render(conn, "index.html", users: users, projects: projects, settings: settings)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/web/views/project_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ProjectView do
2 | use Opencov.Web, :view
3 |
4 | import Opencov.CommonView
5 |
6 | def project_badge_path(conn, project) do
7 | project_badge_path(conn, :badge, project, Application.get_env(:opencov, :badge_format))
8 | end
9 |
10 | def project_badge_url(conn, project) do
11 | project_badge_url(conn, :badge, project, Application.get_env(:opencov, :badge_format))
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/models/job_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.JobTest do
2 | use Opencov.ModelCase
3 |
4 | alias Opencov.Job
5 |
6 | test "compute_coverage" do
7 | job = insert(:job)
8 | insert(:file, job: job, coverage_lines: [0, 1, nil, 0, 2, 1])
9 | insert(:file, job: job, coverage_lines: [0, 0, nil, 0])
10 | coverage = job |> Opencov.Repo.preload(:files) |> Job.compute_coverage
11 | assert coverage == 37.5 # (3 / 8 * 100)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/web/templates/project/new.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Projects", to: project_path(@conn, :index) %>
5 | >
6 |
7 | new
8 |
9 |
10 |
11 |
12 | <%= render "form.html", changeset: @changeset,
13 | action: project_path(@conn, :create) %>
14 |
15 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | services:
4 | postgres:
5 | image: postgres:9.5
6 | volumes:
7 | - ./postgresql-data:/var/lib/postgresql/data/pgdata
8 | environment:
9 | PGDATA: /var/lib/postgresql/data/pgdata
10 | POSTGRES_PASSWORD: 112233
11 |
12 | opencov:
13 | image: danhper/opencov:latest
14 | ports:
15 | - "4000:4000"
16 | volumes:
17 | - ./config/local.sample.exs:/opencov/config/local.exs
18 |
--------------------------------------------------------------------------------
/web/static/css/partials/util.styl:
--------------------------------------------------------------------------------
1 | .left
2 | float left
3 |
4 | .right
5 | float right
6 |
7 | .clearfix
8 | &::after
9 | clear both
10 | display block
11 | content " "
12 |
13 | .coverage
14 | &.na
15 | color #9f9f9f
16 | &.none
17 | color #e05d44
18 | &.low
19 | color #dfb317
20 | &.normal
21 | color #a4a61d
22 | &.good
23 | color #97CA00
24 | &.great
25 | color #4c1
26 |
27 | .text-centered
28 | text-align center
29 |
--------------------------------------------------------------------------------
/web/static/js/components/file-coverage/index.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 | import _ from 'lodash'
3 | import highlight from 'highlight.js'
4 |
5 | import './file-coverage.styl'
6 |
7 | const template = require('./file-coverage.jade')()
8 |
9 | riot.tag('file-coverage', template, function (opts) {
10 | this.file = window.file
11 | const code = highlight.highlightAuto(this.file.source).value
12 | this.coverageInfo = _.zip(code.split('\n'), this.file.coverage)
13 | })
14 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151213015301_create_settings.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Repo.Migrations.CreateSettings do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:settings) do
6 | add :signup_enabled, :boolean, default: false, null: false
7 | add :restricted_signup_domains, :text, default: "", null: false
8 | add :default_project_visibility, :string, default: "internal", null: false
9 |
10 | timestamps()
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/lib/types/json_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Types.JSONText do
2 | use ExUnit.Case
3 |
4 | import Opencov.Types.JSON
5 |
6 | @json String.trim("""
7 | {"foo":"bar"}
8 | """)
9 |
10 | test "load" do
11 | assert load(@json) == {:ok, %{"foo" => "bar"}}
12 | assert {:error, _} = load("invalid_json")
13 | end
14 |
15 | test "dump" do
16 | assert dump(%{"foo" => "bar"}) == {:ok, @json}
17 | assert dump("foo") == {:ok, "foo"}
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/views/job_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.JobViewTest do
2 | use Opencov.ConnCase, async: true
3 |
4 | import Opencov.JobView
5 |
6 | test "job_time when run_at absent" do
7 | job = insert(:job)
8 | assert job_time(job) == job.inserted_at
9 | end
10 |
11 | test "job_time when run_at present" do
12 | job = insert(:job, run_at: Timex.now)
13 | assert job_time(job) == job.run_at
14 | assert job_time(job) != job.inserted_at
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/web/templates/project/form.html.eex:
--------------------------------------------------------------------------------
1 | <%= form_for @changeset, @action, [class: "full-page"], fn f -> %>
2 | <%= if @changeset.action do %>
3 |
4 |
Could not create the project, please check the errors below:
5 |
6 | <% end %>
7 |
8 | <%= input(f, :name) %>
9 | <%= input(f, :base_url) %>
10 |
11 |
12 | <%= submit "Submit", class: "btn btn-primary" %>
13 |
14 | <% end %>
15 |
--------------------------------------------------------------------------------
/web/static/js/components/file-coverage/file-coverage.jade:
--------------------------------------------------------------------------------
1 | table.table.source
2 | caption { file.name }
3 | tbody
4 | tr(
5 | each="{ line, i in coverageInfo }"
6 | class="{covered: line[1] > 0, missed: line[1] === 0}"
7 | )
8 | td.num { i + 1 }
9 | td.code
10 | pre
11 | raw(html="{ line[0] }")
12 | td.hits
13 | span.hits-number(if="{ line[1] > 0 }") { line[1] }x
14 | span.missed-sign(if="{ line[1] === 0 }") ✗
15 |
--------------------------------------------------------------------------------
/web/templates/profile/edit.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Profile
5 |
6 |
7 |
8 | <%= render Opencov.UserView, "form.html", changeset: @changeset,
9 | action: profile_path(@conn, :update),
10 | show_password: false,
11 | is_admin: false %>
12 |
13 |
--------------------------------------------------------------------------------
/web/views/auth_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.AuthView do
2 | use Opencov.Web, :view
3 |
4 | import Opencov.Helpers.Authentication
5 |
6 | def initial_value(form, param) do
7 | compute_value(param, Map.get(form.params, to_string(param), ""), demo?())
8 | end
9 |
10 | defp compute_value(_param, value, false), do: value
11 | defp compute_value(param, "", true), do: Application.get_env(:opencov, :demo)[param]
12 | defp compute_value(_param, value, _), do: value
13 | end
14 |
--------------------------------------------------------------------------------
/lib/opencov/plug/anonymous_only.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Plug.AnonymousOnly do
2 | import Opencov.Helpers.Authentication
3 | import Plug.Conn, only: [halt: 1]
4 | import Phoenix.Controller, only: [redirect: 2]
5 |
6 | def init(opts) do
7 | Keyword.put_new(opts, :redirect_to, "/")
8 | end
9 |
10 | def call(conn, opts) do
11 | if user_signed_in?(conn) do
12 | redirect(conn, to: opts[:redirect_to]) |> halt
13 | else
14 | conn
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151108002255_create_badge.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Repo.Migrations.CreateBadge do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:badges) do
6 | add :project_id, :integer
7 | add :image, :binary
8 | add :format, :string
9 | add :coverage, :float
10 |
11 | timestamps()
12 | end
13 |
14 | create index(:badges, [:project_id])
15 | create unique_index(:badges, [:project_id, :format])
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/web/controllers/build_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BuildController do
2 | use Opencov.Web, :controller
3 |
4 | alias Opencov.Build
5 | alias Opencov.FileService
6 |
7 | def show(conn, %{"id" => id} = params) do
8 | build = Repo.get!(Build, id) |> Repo.preload([:jobs, :project])
9 | job_ids = Enum.map build.jobs, &(&1.id)
10 | file_params = FileService.files_with_filter(job_ids, params)
11 | render(conn, "show.html", [{:build, build}|file_params])
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/web/templates/admin/shared/actions.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Edit", to: @path_fn.(@conn, :edit, @resource), class: "btn btn-primary btn-xl" %>
5 |
6 |
7 | <%= link "Delete", to: @path_fn.(@conn, :delete, @resource), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xl" %>
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/web/static/css/theme.less:
--------------------------------------------------------------------------------
1 | @import "~bootstrap/less/bootstrap";
2 |
3 |
4 | @body-bg: #fafafa;
5 |
6 | @brand-primary: darken(#A9C696, 20%);
7 |
8 |
9 | @navbar-inverse-link-color: #fff;
10 | @navbar-inverse-link-hover-color: #eee;
11 | @navbar-inverse-brand-hover-color: @navbar-inverse-link-hover-color;
12 | @navbar-inverse-bg: #A9C696;
13 | @navbar-inverse-border: darken(@navbar-inverse-bg, 10%);
14 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151022125349_create_project.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Repo.Migrations.CreateProject do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:projects) do
6 | add :name, :string
7 | add :token, :string
8 | add :user_id, :integer
9 | add :current_coverage, :float
10 | add :base_url, :string
11 |
12 | timestamps()
13 | end
14 |
15 | create index(:projects, [:user_id])
16 | create unique_index(:projects, [:token])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/web/templates/mailers/user/reset_password.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reset your password
6 |
11 |
12 |
13 |
14 | Hi <%= user.name %>,
15 |
16 | Thank you for using Opencov.
17 |
18 |
19 | Please click here to reset your password.
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/web/templates/profile/edit_password.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Change password
5 |
6 |
7 |
8 | <%= render "password_form.html", changeset: @changeset,
9 | hide_current_password: false,
10 | token: nil,
11 | action: profile_path(@conn, :update_password),
12 | user: @user %>
13 |
14 |
--------------------------------------------------------------------------------
/web/templates/profile/reset_password.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Reset password
5 |
6 |
7 |
8 | <%= render "password_form.html", changeset: @changeset,
9 | hide_current_password: true,
10 | token: @token,
11 | action: profile_path(@conn, :finalize_reset_password),
12 | user: @user %>
13 |
14 |
--------------------------------------------------------------------------------
/lib/opencov/plug/fetch_user.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Plug.FetchUser do
2 | import Plug.Conn
3 |
4 | def init(opts) do
5 | opts
6 | end
7 |
8 | def call(conn, _opts) do
9 | if user = current_user(conn) do
10 | %{conn | assigns: Map.put(conn.assigns, :current_user, user)}
11 | else
12 | conn
13 | end
14 | end
15 |
16 | defp current_user(conn) do
17 | if user_id = get_session(conn, Opencov.Authentication.user_id_key) do
18 | Opencov.Repo.get(Opencov.User, user_id)
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/web/templates/mailers/user/confirmation.text.eex:
--------------------------------------------------------------------------------
1 | Hi <%= user.name %>,
2 | <%= if opts[:registration] do %>
3 | <%= if opts[:invited] do %>
4 | You have been invited to join Opencov@<%= base_url %>
5 | <% else %>
6 | Thank you for registering to Opencov@<%= base_url %>
7 | <% end %>
8 |
9 | Your login address is: <%= user.email %>
10 | <% end %>
11 |
12 | <%= if opts[:invited] do %>
13 | Please visit the following address to set your password.
14 | <% else %>
15 | Please visit the following address to confirm your email.
16 | <% end %>
17 |
18 | <%= confirmation_url %>
19 |
--------------------------------------------------------------------------------
/web/templates/project/edit.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Projects", to: project_path(@conn, :index) %>
5 | >
6 | <%= link @project.name, to: project_path(@conn, :show, @project) %>
7 | >
8 | edit
9 |
10 |
11 |
12 | <%= render "form.html", changeset: @changeset,
13 | action: project_path(@conn, :update, @project) %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/models/build_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BuildTest do
2 | use Opencov.ModelCase
3 |
4 | alias Opencov.Build
5 |
6 | test "current_for_project when no build exist" do
7 | insert(:build, completed: false)
8 | refute Build.current_for_project(Build, insert(:project)) |> Repo.first
9 | end
10 |
11 | test "current_for_project when build exists" do
12 | existing_build = insert(:build, completed: false) |> Repo.preload(:project)
13 | build = Build.current_for_project(Build, existing_build.project) |> Repo.first
14 | assert build.id == existing_build.id
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/web/controllers/admin/project_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Admin.ProjectController do
2 | use Opencov.Web, :controller
3 |
4 | alias Opencov.Project
5 | alias Opencov.Repo
6 |
7 | plug :scrub_params, "project" when action in [:create, :update]
8 |
9 | def index(conn, params) do
10 | paginator = Repo.paginate(Project, params)
11 | render(conn, "index.html", projects: paginator.entries, paginator: paginator)
12 | end
13 |
14 | def show(conn, %{"id" => id}) do
15 | project = Repo.get!(Project, id)
16 | render(conn, "show.html", project: project)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/opencov/helpers/display.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Helpers.Display do
2 | def display(nil), do: "-"
3 | def display(value) when is_boolean(value), do: bool(value)
4 | def display(value) when is_atom(value), do: atom(value)
5 | def display(value) when is_binary(value), do: text(value)
6 |
7 | def bool(true), do: "✔"
8 | def bool(_), do: "×"
9 |
10 | def atom(a) when is_atom(a) do
11 | a |> Atom.to_string |> String.split("_") |> Enum.join(" ") |> String.capitalize
12 | end
13 |
14 | def text(text) do
15 | {:safe, text |> String.split("\n") |> Enum.join(" ")}
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/controllers/file_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.FileControllerTest do
2 | use Opencov.ConnCase
3 |
4 | setup do
5 | conn = build_conn() |> with_login
6 | {:ok, conn: conn}
7 | end
8 |
9 | test "shows chosen resource", %{conn: conn} do
10 | file = insert(:file)
11 | conn = get conn, file_path(conn, :show, file)
12 | assert html_response(conn, 200) =~ file.name
13 | end
14 |
15 | test "renders page not found when id is nonexistent", %{conn: conn} do
16 | assert_raise Ecto.NoResultsError, fn ->
17 | get conn, file_path(conn, :show, -1)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/web/static/js/components/badges/index.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 |
3 | import './badges.styl'
4 |
5 | const template = require('./badges.jade')()
6 |
7 | riot.tag('badges', template, function (opts) {
8 | const splitted = opts.badgeUrl.split('.')
9 | const baseURL = splitted.slice(0, splitted.length - 1).join('.')
10 |
11 | const setFormat = (format) => {
12 | this.format = format
13 | this.badgeURL = [baseURL, this.format].join('.')
14 | }
15 |
16 | setFormat(splitted[splitted.length - 1])
17 |
18 | this.handleClickFormat = (e) => {
19 | setFormat(e.target.dataset.format)
20 | }
21 | })
22 |
--------------------------------------------------------------------------------
/web/static/js/components/project-token/index.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 |
3 | import './project-token.styl'
4 |
5 | const template = require('./project-token.jade')()
6 |
7 | riot.tag('project-token', template, function (opts) {
8 | this.token = opts.token
9 | this.shown = false
10 |
11 | this.toggleShown = () => {
12 | this.shown = !this.shown
13 | if (this.shown) {
14 | setTimeout(() => this['token-input'].select(), 10)
15 | } else {
16 | this.root.children[0].blur()
17 | }
18 | }
19 |
20 | this.computeWidth = () => {
21 | return this.token.length * 5
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/web/templates/profile/reset_password_request.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Reset password
5 |
6 |
7 |
8 | <%= form_for @conn, profile_path(@conn, :send_reset_password), [as: :user, class: "full-page"], fn f -> %>
9 |
10 | <%= label f, :email, "Email", class: "control-label" %>
11 | <%= text_input f, :email, class: "form-control" %>
12 |
13 |
14 |
15 | <%= submit "Reset password", class: "btn btn-primary" %>
16 |
17 | <% end %>
18 |
19 |
--------------------------------------------------------------------------------
/web/models/project.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Project do
2 | use Opencov.Web, :model
3 |
4 | import Ecto.Query
5 |
6 | schema "projects" do
7 | field :name, :string
8 | field :token, :string
9 | field :current_coverage, :float
10 | field :base_url, :string
11 |
12 | belongs_to :user, Opencov.User
13 | has_many :builds, Opencov.Build
14 | has_one :badge, Opencov.Badge
15 |
16 | timestamps()
17 | end
18 |
19 | def with_token(query, token) do
20 | query |> where(token: ^token)
21 | end
22 |
23 | def visibility_choices do
24 | ~w(public private internal)
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/test/views/file_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.FileViewTest do
2 | use Opencov.ConnCase, async: true
3 |
4 | import Opencov.FileView, only: [filters: 0, short_name: 1]
5 |
6 | test "filters" do
7 | assert Enum.count(filters()) == 4
8 | end
9 |
10 | test "short_name" do
11 | assert short_name("foo/bar") == "foo/bar"
12 | assert short_name("foo/bar/baz") == "foo/bar/baz"
13 | f15 = String.duplicate("f", 15)
14 | f20 = String.duplicate("f", 20)
15 | assert short_name("foo/bar/baz/#{f20}") == "f/b/b/#{f20}"
16 | assert short_name("foo/bar/baz/#{f15}") == "f/b/baz/#{f15}"
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/priv/repo/seeds/eyecatch/0012_jobs.exs:
--------------------------------------------------------------------------------
1 | import Seedex
2 |
3 | seed Opencov.Job, fn job ->
4 | job
5 | |> Map.put(:id, 1)
6 | |> Map.put(:build_id, 1)
7 | |> Map.put(:coverage, 72.8)
8 | |> Map.put(:files_count, 11)
9 | |> Map.put(:job_number, 1)
10 | |> Map.put(:run_at, Timex.now())
11 | end
12 |
13 | seed Opencov.Job, fn job ->
14 | job
15 | |> Map.put(:id, 2)
16 | |> Map.put(:build_id, 2)
17 | |> Map.put(:coverage, 74.24)
18 | |> Map.put(:files_count, 11)
19 | |> Map.put(:job_number, 2)
20 | |> Map.put(:previous_job_id, 1)
21 | |> Map.put(:previous_coverage, 72.8)
22 | |> Map.put(:run_at, Timex.now())
23 | end
24 |
--------------------------------------------------------------------------------
/test/lib/badge_creator_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BadgeCreatorTest do
2 | use ExUnit.Case
3 |
4 | import Mock
5 |
6 | alias Opencov.BadgeCreator
7 |
8 | test "make_badge creates a new badge" do
9 | {:ok, format, output} = BadgeCreator.make_badge(50, format: :svg)
10 | assert format == :svg
11 | assert String.contains?(output, "50")
12 | end
13 |
14 | test "make badge in other format" do
15 | with_mock Opencov.ImageMagick, [convert: fn([input, output]) -> File.copy!(input, output) end] do
16 | {:ok, format, _} = BadgeCreator.make_badge(50, format: :png)
17 | assert format == :png
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/opencov/core.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Core do
2 | defmacro __using__(_opts) do
3 | quote do
4 | import Opencov.Core, only: [pipe_when: 3]
5 | end
6 | end
7 |
8 | defmacro pipe_when(left, condition, fun) do
9 | quote do
10 | if Opencov.Core.should_pipe(left, unquote(condition)) do
11 | unquote(left) |> unquote(fun)
12 | else
13 | unquote(left)
14 | end
15 | end
16 | end
17 |
18 | defmacro should_pipe(left, condition) when is_function(condition) do
19 | quote do
20 | unquote(left) |> unquote(condition)
21 | end
22 | end
23 | defmacro should_pipe(_, condition), do: condition
24 | end
25 |
--------------------------------------------------------------------------------
/test/controllers/build_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BuildControllerTest do
2 | use Opencov.ConnCase
3 |
4 | setup do
5 | conn = build_conn() |> with_login
6 | {:ok, conn: conn}
7 | end
8 |
9 | test "shows chosen resource", %{conn: conn} do
10 | build = insert(:build) |> Repo.preload(:project)
11 | conn = get conn, build_path(conn, :show, build)
12 | assert html_response(conn, 200) =~ build.project.name
13 | end
14 |
15 | test "renders page not found when id is nonexistent", %{conn: conn} do
16 | assert_raise Ecto.NoResultsError, fn ->
17 | get conn, build_path(conn, :show, -1)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/web/static/css/pages/admin.styl:
--------------------------------------------------------------------------------
1 | .admin
2 | .panel
3 | &.col-sm-5
4 | &:nth-child(odd)
5 | clear both
6 | padding 0
7 | .panel-title
8 | line-height 2em
9 | & > a.btn
10 | color #fff
11 |
12 | .view-all
13 | display block
14 | margin 5px 0 10px 10px
15 |
16 | dl
17 | dt, dd
18 | line-height 3em
19 | &.settings
20 | dt
21 | width 200px
22 | dd
23 | margin-left 220px
24 |
25 | dt.restricted_signup_domains + dd
26 | padding-top 0.7em
27 | line-height 1.5em
28 |
29 | &.edit-settings
30 | textarea
31 | resize vertical
32 | min-height 8em
33 |
--------------------------------------------------------------------------------
/test/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ErrorViewTest do
2 | use Opencov.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(Opencov.ErrorView, "404.html", []) ==
9 | "Page not found"
10 | end
11 |
12 | test "render 500.html" do
13 | assert render_to_string(Opencov.ErrorView, "500.html", []) ==
14 | "Server internal error"
15 | end
16 |
17 | test "render any other" do
18 | assert render_to_string(Opencov.ErrorView, "505.html", []) ==
19 | "Server internal error"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/web/templates/admin/user/new.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Admin", to: admin_dashboard_path(@conn, :index) %>
5 | >
6 | <%= link "Users", to: admin_user_path(@conn, :index) %>
7 | >
8 | new
9 |
10 |
11 |
12 |
13 |
14 | <%= render Opencov.UserView, "form.html",
15 | changeset: @changeset,
16 | action: admin_user_path(@conn, :create),
17 | show_password: false,
18 | is_admin: true %>
19 |
20 |
--------------------------------------------------------------------------------
/web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ErrorView do
2 | use Opencov.Web, :view
3 |
4 | def render("404.html", _assigns) do
5 | "Page not found"
6 | end
7 |
8 | def render("404.json", assigns) do
9 | message = case assigns.reason do
10 | %Ecto.NoResultsError{} -> "could not find model"
11 | _ -> "no such path"
12 | end
13 | %{error: message}
14 | end
15 |
16 | def render("500.html", _assigns) do
17 | "Server internal error"
18 | end
19 |
20 | # In case no render clause matches or no
21 | # template is found, let's render it as 500
22 | def template_not_found(_template, assigns) do
23 | render "500.html", assigns
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151022144345_create_file.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Repo.Migrations.CreateFile do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:files) do
6 | add :job_id, :integer, null: false
7 | add :name, :string, null: false
8 | add :source, :text, null: false
9 | add :coverage_lines, :text, null: false
10 | add :coverage, :float, null: false
11 | add :previous_coverage, :float
12 | add :previous_file_id, :integer
13 |
14 | timestamps()
15 | end
16 |
17 | create index(:files, [:job_id])
18 | create unique_index(:files, [:job_id, :name])
19 | create index(:files, [:previous_file_id])
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151022143636_create_job.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Repo.Migrations.CreateJob do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:jobs) do
6 | add :build_id, :integer, null: false
7 | add :job_number, :integer, null: false
8 | add :coverage, :float, null: false, default: 0.0
9 | add :previous_coverage, :float
10 | add :previous_job_id, :integer
11 |
12 | add :run_at, :utc_datetime
13 | add :files_count, :integer
14 |
15 | timestamps()
16 | end
17 |
18 | create index(:jobs, [:build_id])
19 | create unique_index(:jobs, [:build_id, :job_number])
20 | create index(:jobs, [:previous_job_id])
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/web/static/css/partials/header.styl:
--------------------------------------------------------------------------------
1 | .body-wrapper
2 | .navbar
3 | &.navbar-inverse
4 | margin-bottom 0
5 | .navbar-brand
6 | font-size 1.6em
7 | & > img
8 | display inline
9 | max-height 100%
10 | height 100%
11 | -o-object-fit contain
12 | object-fit contain
13 | .navbar-right
14 | .avatar
15 | border-radius 12px
16 | min-height 24px
17 | min-width 24px
18 |
19 | .dropdown-menu > li
20 | & > a, & > form > a
21 | display block
22 | padding 3px 20px
23 | clear both
24 | font-weight normal
25 | line-height 30px
26 | color #333333
27 | white-space nowrap
28 |
--------------------------------------------------------------------------------
/test/managers/settings_manager_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.SettingsManagerTest do
2 | use Opencov.ManagerCase
3 |
4 | alias Opencov.Settings
5 | alias Opencov.SettingsManager
6 |
7 | @valid_attrs %{default_project_visibility: "internal", restricted_signup_domains: "some content", signup_enabled: true}
8 | @invalid_attrs %{default_project_visibility: "foobar"}
9 |
10 | test "changeset with valid attributes" do
11 | changeset = SettingsManager.changeset(%Settings{}, @valid_attrs)
12 | assert changeset.valid?
13 | end
14 |
15 | test "changeset with invalid attributes" do
16 | changeset = SettingsManager.changeset(%Settings{}, @invalid_attrs)
17 | refute changeset.valid?
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/web/models/user.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.User do
2 | use Opencov.Web, :model
3 |
4 | use SecurePassword
5 |
6 | schema "users" do
7 | field :email, :string
8 | field :admin, :boolean, default: false
9 | field :name, :string
10 | field :password_initialized, :boolean, default: true
11 | field :confirmation_token, :string
12 | field :confirmed_at, :utc_datetime
13 | field :unconfirmed_email, :string
14 |
15 | field :password_reset_token, :string
16 | field :password_reset_sent_at, :utc_datetime
17 |
18 | field :current_password, :string, virtual: true
19 | has_secure_password()
20 |
21 | has_many :projects, Opencov.Project
22 |
23 | timestamps()
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/web/static/js/components/badges/badges.jade:
--------------------------------------------------------------------------------
1 | ul.nav.nav-pills.formats
2 | li(class="{active: format === 'svg'}")
3 | a(href="javascript:void(0)" data-format="svg" onclick="{ handleClickFormat }") SVG
4 | li(class="{active: format === 'png'}")
5 | a(href="javascript:void(0)" data-format="png" onclick="{ handleClickFormat }") PNG
6 | dl.badge-list
7 | dt URL
8 | dd
9 | selectable(text="{ badgeURL }" rows="1")
10 | dt Markdown
11 | dd
12 | selectable(text="[]({ opts.projectUrl })" rows="3")
13 | dt HTML
14 | dd
15 | selectable(
16 | text=" "
17 | rows="4"
18 | )
19 |
--------------------------------------------------------------------------------
/config/local.sample.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :opencov, Opencov.Endpoint,
4 | http: [port: 4000],
5 | url: [scheme: "http", host: "demo.opencov.com", port: 80],
6 | secret_key_base: "my-super-secret-key-base-with-64-characters-so-that-i-dont-get-an-error"
7 |
8 | config :opencov, Opencov.Repo,
9 | adapter: Ecto.Adapters.Postgres,
10 | url: "postgres://postgres:112233@postgres/opencov_prod?ssl=false",
11 | pool_size: 20
12 |
13 | config :opencov, :email,
14 | sender: "OpenCov ",
15 | smtp: [
16 | relay: "smtp.example.com",
17 | username: "info@example.com",
18 | password: "my-ultra-secret-password",
19 | port: 587,
20 | ssl: false,
21 | tls: :always,
22 | auth: :always
23 | ]
24 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :opencov, Opencov.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
12 | config :excoveralls, :endpoint, System.get_env("COVERALLS_ENDPOINT") || "http://demo.opencov.com"
13 |
14 | config :comeonin,
15 | bcrypt_log_rounds: 4,
16 | pbkdf2_rounds: 1
17 |
18 | # Configure your database
19 | config :opencov, Opencov.Repo,
20 | adapter: Ecto.Adapters.Postgres,
21 | username: "postgres",
22 | password: "postgres",
23 | database: "opencov_test",
24 | hostname: "localhost",
25 | pool: Ecto.Adapters.SQL.Sandbox
26 |
--------------------------------------------------------------------------------
/web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 | By using [Gettext](https://hexdocs.pm/gettext),
5 | your module gains a set of macros for translations, for example:
6 | import Opencov.Gettext
7 | # Simple translation
8 | gettext "Here is the string to translate"
9 | # Plural translation
10 | ngettext "Here is the string to translate",
11 | "Here are the strings to translate",
12 | 3
13 | # Domain-based translation
14 | dgettext "errors", "Here is the error message to translate"
15 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
16 | """
17 | use Gettext, otp_app: :opencov
18 | end
19 |
--------------------------------------------------------------------------------
/lib/opencov/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Repo do
2 | use Ecto.Repo, otp_app: :opencov
3 | use Scrivener, page_size: 10
4 |
5 | require Ecto.Query
6 | alias Ecto.Query
7 |
8 | def latest(model, opts \\ []) do
9 | all(Query.from m in model,
10 | select: m,
11 | limit: ^Keyword.get(opts, :limit, 5),
12 | order_by: [desc: field(m, ^Keyword.get(opts, :order, :inserted_at))]
13 | )
14 | end
15 |
16 |
17 | def first(queryable, opts \\ [])
18 | def first(nil, _opts), do: nil
19 | def first(queryable, opts) do
20 | queryable |> Ecto.Query.first |> one(opts)
21 | end
22 |
23 | def first!(queryable, opts \\ [])
24 | def first!(nil, _opts), do: nil
25 | def first!(queryable, opts) do
26 | queryable |> Ecto.Query.first |> one!(opts)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | env:
2 | node: true
3 | es6: true
4 | browser: true
5 |
6 | parserOptions:
7 | sourceType: module
8 |
9 | rules:
10 | no-debugger: 2
11 | no-dupe-args: 2
12 | no-dupe-keys: 2
13 | no-duplicate-case: 2
14 | no-ex-assign: 2
15 | no-unreachable: 2
16 | valid-typeof: 2
17 | no-fallthrough: 2
18 | quotes: [2, "single", "avoid-escape"]
19 | indent: [2, 2]
20 | comma-spacing: 2
21 | semi: [2, "never"]
22 | space-infix-ops: 2
23 | keyword-spacing: 2
24 | space-before-function-paren: [2, {named: "never"}]
25 | space-before-blocks: [2, "always"]
26 | new-parens: 2
27 | max-len: [2, 80, 2]
28 | no-multiple-empty-lines: [2, {max: 2}]
29 | eol-last: 2
30 | no-trailing-spaces: 2
31 | prefer-const: 2
32 | strict: [2, "global"]
33 | no-undef: 2
34 |
35 | globals:
36 | $: true
37 |
--------------------------------------------------------------------------------
/web/templates/profile/password_form.html.eex:
--------------------------------------------------------------------------------
1 | <%= form_for @changeset, @action, [class: "full-page"], fn f -> %>
2 | <%= if @changeset.action do %>
3 |
4 |
Failed to update your password, check the errors below
5 |
6 | <% end %>
7 |
8 | <%= if @user.password_initialized and not @hide_current_password do %>
9 | <%= input(f, :current_password, type: :password_input) %>
10 | <% end %>
11 |
12 | <%= input(f, :password, type: :password_input) %>
13 | <%= input(f, :password_confirmation, type: :password_input) %>
14 |
15 | <%= if @token do %>
16 | <%= hidden_input f, :password_reset_token, value: @token %>
17 | <% end %>
18 |
19 |
20 | <%= submit "Change password", class: "btn btn-primary" %>
21 |
22 | <% end %>
23 |
--------------------------------------------------------------------------------
/test/controllers/job_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.JobControllerTest do
2 | use Opencov.ConnCase
3 |
4 | setup do
5 | conn = build_conn() |> with_login
6 | {:ok, conn: conn}
7 | end
8 |
9 | test "redirects when not logged in" do
10 | job = insert(:job)
11 | conn = build_conn()
12 | conn = get conn, job_path(conn, :show, job)
13 | assert redirected_to(conn) == auth_path(conn, :login)
14 | end
15 |
16 | test "shows chosen resource", %{conn: conn} do
17 | job = insert(:job)
18 | conn = get conn, job_path(conn, :show, job)
19 | assert html_response(conn, 200) =~ "#{job.job_number}"
20 | end
21 |
22 | test "renders page not found when id is nonexistent", %{conn: conn} do
23 | assert_raise Ecto.NoResultsError, fn ->
24 | get conn, job_path(conn, :show, -1)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/web/models/badge.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Badge do
2 | use Opencov.Web, :model
3 |
4 | import Ecto.Query
5 |
6 | schema "badges" do
7 | field :image, :binary
8 | field :format, :string
9 | field :coverage, :float
10 |
11 | belongs_to :project, Opencov.Project
12 |
13 | timestamps()
14 | end
15 |
16 | def for_project(query, %Opencov.Project{id: project_id}),
17 | do: for_project(query, project_id)
18 | def for_project(query, project_id) when is_integer(project_id),
19 | do: query |> where(project_id: ^project_id)
20 |
21 | def with_format(query, format) when is_atom(format),
22 | do: with_format(query, Atom.to_string(format))
23 | def with_format(query, format),
24 | do: query |> where(format: ^format)
25 |
26 | def default_format(),
27 | do: Application.get_env(:opencov, :badge_format)
28 | end
29 |
--------------------------------------------------------------------------------
/web/templates/admin/user/edit.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Admin", to: admin_dashboard_path(@conn, :index) %>
5 | >
6 | <%= link "Users", to: admin_user_path(@conn, :index) %>
7 | >
8 | <%= link @user.name, to: admin_user_path(@conn, :show, @user) %>
9 | >
10 | edit
11 |
12 |
13 |
14 |
15 | <%= render Opencov.UserView, "form.html",
16 | changeset: @changeset,
17 | action: admin_user_path(@conn, :update, @user),
18 | show_password: true,
19 | is_admin: true %>
20 |
21 |
22 |
--------------------------------------------------------------------------------
/web/models/job.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Job do
2 | use Opencov.Web, :model
3 |
4 | import Ecto.Query
5 |
6 | schema "jobs" do
7 | field :coverage, :float, default: 0.0
8 | field :previous_job_id, :integer
9 | field :run_at, :utc_datetime
10 | field :files_count, :integer
11 | field :job_number, :integer
12 | field :previous_coverage, :float
13 |
14 | belongs_to :build, Opencov.Build
15 | has_one :previous_job, Opencov.Job
16 | has_many :files, Opencov.File
17 |
18 | timestamps()
19 | end
20 |
21 | def compute_coverage(job) do
22 | lines = Enum.flat_map job.files, &(&1.coverage_lines)
23 | Opencov.File.compute_coverage(lines)
24 | end
25 |
26 | def for_build(query, %Opencov.Build{id: id}), do: for_build(query, id)
27 | def for_build(query, build_id) when is_integer(build_id),
28 | do: query |> where(build_id: ^build_id)
29 | end
30 |
--------------------------------------------------------------------------------
/web/services/file_service.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.FileService do
2 | use Opencov.Web, :service
3 |
4 | alias Opencov.File
5 |
6 | # FIXME: we should make file distinct by name instead but this is not yet
7 | # supported by scrivener
8 | def files_with_filter([job|_], params), do: files_with_filter(job, params)
9 | def files_with_filter(job, params) do
10 | filters = Map.get(params, "filters", [])
11 | order_field = Map.get(params, "order_field", "diff")
12 | order_direction = Map.get(params, "order_direction", :desc)
13 | query = File.for_job(job) |> File.with_filters(filters) |> File.sort_by(order_field, order_direction)
14 | paginator = query |> Opencov.Repo.paginate(params)
15 | [
16 | filters: filters,
17 | paginator: paginator,
18 | files: paginator.entries,
19 | order: {order_field, order_direction}
20 | ]
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/opencov/plug/authentication.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Plug.Authentication do
2 | import Plug.Conn
3 | import Phoenix.Controller, only: [redirect: 2, put_flash: 3]
4 | import Opencov.Helpers.Authentication
5 |
6 | def init(opts) do
7 | opts
8 | end
9 |
10 | def call(conn, opts) do
11 | if user_signed_in?(conn) do
12 | check_admin(conn, opts)
13 | else
14 | redirect_with(conn, :info, "Please login", "/login")
15 | end
16 | end
17 |
18 | defp check_admin(conn, opts) do
19 | if current_user(conn).admin || !opts[:admin] do
20 | conn
21 | else
22 | redirect_with(conn, :error, "You are not authorized here.", "/")
23 | end
24 | end
25 |
26 | defp redirect_with(conn, flash_type, flash_message, path) do
27 | conn
28 | |> put_flash(flash_type, flash_message)
29 | |> redirect(to: path)
30 | |> halt
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/opencov/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :opencov
3 |
4 | plug Plug.Static,
5 | at: "/", from: :opencov, gzip: false,
6 | only: ~w(css fonts images js favicon.ico robots.txt)
7 |
8 | if code_reloading? do
9 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
10 | plug Phoenix.LiveReloader
11 | plug Phoenix.CodeReloader
12 | end
13 |
14 | unless Mix.env == :test do
15 | plug Plug.RequestId
16 | plug Plug.Logger
17 | end
18 |
19 | plug Plug.Parsers,
20 | parsers: [:urlencoded, :multipart, :json],
21 | pass: ["*/*"],
22 | json_decoder: Jason,
23 | length: 100_000_000
24 |
25 | plug Plug.MethodOverride
26 | plug Plug.Head
27 |
28 | plug Plug.Session,
29 | store: :cookie,
30 | key: "_opencov_key",
31 | signing_salt: "DBdPx/m/"
32 |
33 | plug Opencov.Router
34 | end
35 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151202133406_create_user.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Repo.Migrations.CreateUser do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:users) do
6 | add :name, :string
7 | add :email, :string
8 | add :password_digest, :string
9 | add :admin, :boolean, default: false
10 |
11 | add :password_initialized, :boolean, default: false
12 |
13 | add :confirmation_token, :string
14 | add :confirmed_at, :utc_datetime
15 | add :unconfirmed_email, :string
16 |
17 | add :password_reset_token, :string
18 | add :password_reset_sent_at, :utc_datetime
19 |
20 | timestamps()
21 | end
22 |
23 | create unique_index(:users, [:email])
24 | create unique_index(:users, [:unconfirmed_email])
25 | create unique_index(:users, [:confirmation_token])
26 | create unique_index(:users, [:password_reset_token])
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/web/views/file_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.FileView do
2 | use Opencov.Web, :view
3 |
4 | import Opencov.CommonView
5 | import Scrivener.HTML
6 |
7 | @max_length 20
8 |
9 | def filters do
10 | %{
11 | "changed" => "Changed",
12 | "cov_changed" => "Coverage changed",
13 | "covered" => "Covered",
14 | "unperfect" => "Unperfect"
15 | }
16 | end
17 |
18 | def short_name(name) do
19 | if String.length(name) < @max_length do
20 | name
21 | else
22 | name
23 | |> String.split("/")
24 | |> Enum.reverse
25 | |> Enum.reduce({[], 0}, fn s, {n, len} ->
26 | if len + String.length(s) <= @max_length do
27 | {[s|n], len + String.length(s)}
28 | else
29 | {[String.first(s)|n], len + 1}
30 | end
31 | end)
32 | |> elem(0)
33 | |> Enum.join("/")
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/opencov/templates/badge_template.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | coverage
7 | coverage
8 | <%= coverage_str %>
9 | <%= coverage_str %>
10 |
11 |
12 |
--------------------------------------------------------------------------------
/web/templates/file/show.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link @file.job.build.project.name, to: project_path(@conn, :show, @file.job.build.project) %>
4 | >
5 |
6 | <%= link "#{@file.job.build.build_number}", to: build_path(@conn, :show, @file.job.build) %>
7 |
8 | .
9 |
10 | <%= link "#{@file.job.job_number}", to: job_path(@conn, :show, @file.job) %>
11 |
12 | >
13 |
14 | <%= short_name @file.name %>
15 |
16 |
17 |
18 |
19 | <%= format_coverage(@file.coverage) %>
20 |
21 |
22 |
23 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/web/templates/auth/login.html.eex:
--------------------------------------------------------------------------------
1 | <%= form_for @conn, auth_path(@conn, :login), [as: :login], fn f -> %>
2 | <%= if @error do %>
3 |
4 | <%= @error %>
5 |
6 | <% end %>
7 |
8 | <%= if demo?() do %>
9 | This is a demo, you can enter with the prefilled email/password
10 | (email: user@opencov.com, password: password123)
11 |
12 | <% end %>
13 |
14 | <%= input(f, :email, scope: "user", attrs: [value: initial_value(f, :email)]) %>
15 | <%= input(f, :password, type: :password_input, scope: :user,
16 | attrs: [value: initial_value(f, :password)]) %>
17 |
18 |
19 |
20 | <%= submit "Login", class: "btn btn-primary" %>
21 |
22 | <% end %>
23 |
24 | <%= link "Forgot your password?", to: profile_path(@conn, :reset_password_request) %>
25 | <%= if @can_signup do %>
26 | <%= link "Create an account", to: user_path(@conn, :new) %>
27 | <% end %>
28 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :opencov, Opencov.Endpoint,
4 | http: [port: {:system, "PORT"}],
5 | url: [
6 | scheme: System.get_env("OPENCOV_SCHEME") || "https",
7 | host: System.get_env("OPENCOV_HOST") || "demo.opencov.com",
8 | port: System.get_env("OPENCOV_PORT") || 443
9 | ],
10 | secret_key_base: System.get_env("SECRET_KEY_BASE")
11 |
12 | config :opencov, Opencov.Repo,
13 | adapter: Ecto.Adapters.Postgres,
14 | url: System.get_env("DATABASE_URL"),
15 | pool_size: String.to_integer(System.get_env("POSTGRES_POOL_SIZE") || "10"),
16 | ssl: true
17 |
18 | config :opencov, :auth,
19 | enable: System.get_env("OPENCOV_AUTH") == "true",
20 | username: System.get_env("OPENCOV_USER"),
21 | password: System.get_env("OPENCOV_PASSWORD"),
22 | realm: System.get_env("OPENCOV_REALM") || "Protected OpenCov"
23 |
24 | config :logger, level: :info
25 |
26 | if File.exists?(Path.join(__DIR__, "prod.secret.exs")) do
27 | import_config "prod.secret.exs"
28 | end
29 |
--------------------------------------------------------------------------------
/web/templates/job/show.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= link @job.build.project.name, to: project_path(@conn, :show, @job.build.project) %>
4 | >
5 |
6 | <%= link "#{@job.build.build_number}", to: build_path(@conn, :show, @job.build) %>
7 |
8 | .
9 |
10 | <%= @job.job_number %>
11 |
12 |
13 |
14 |
15 | <%= format_coverage(@job.coverage) %>
16 |
17 |
18 |
19 | <%= render Opencov.FileView,
20 | "list.html",
21 | conn: @conn,
22 | paginator: @paginator,
23 | files: @files,
24 | order: @order,
25 | filters: @filters,
26 | path_fn: &Opencov.Router.Helpers.job_path/4,
27 | path_args: [@conn, :show, @job]
28 | %>
29 |
--------------------------------------------------------------------------------
/web/controllers/admin/settings_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Admin.SettingsController do
2 | use Opencov.Web, :controller
3 |
4 | alias Opencov.SettingsManager
5 | alias Opencov.Repo
6 |
7 | # plug :scrub_params, "settings" when action in [:update]
8 |
9 | def edit(conn, _params) do
10 | settings = SettingsManager.get!
11 | changeset = SettingsManager.changeset(settings)
12 | render(conn, "edit.html", settings: settings, changeset: changeset)
13 | end
14 |
15 | def update(conn, %{"settings" => settings_params}) do
16 | settings = SettingsManager.get!
17 | changeset = SettingsManager.changeset(settings, settings_params)
18 |
19 | case Repo.update(changeset) do
20 | {:ok, _settings} ->
21 | conn
22 | |> put_flash(:info, "Settings updated successfully.")
23 | |> redirect(to: admin_dashboard_path(conn, :index))
24 | {:error, changeset} ->
25 | render(conn, "edit.html", settings: settings, changeset: changeset)
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.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 | imports other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | alias Opencov.Repo
24 | import Ecto.Query, only: [from: 2]
25 |
26 |
27 | # The default endpoint for testing
28 | @endpoint Opencov.Endpoint
29 | end
30 | end
31 |
32 | setup _tags do
33 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Opencov.Repo)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/opencov/plug/force_password_initialize.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Plug.ForcePasswordInitialize do
2 | import Opencov.Helpers.Authentication
3 | import Plug.Conn, only: [halt: 1]
4 | import Phoenix.Controller, only: [redirect: 2]
5 |
6 | def init(opts) do
7 | opts
8 | end
9 |
10 | def call(conn, _opts) do
11 | if user_signed_in?(conn) do
12 | check_password_state(conn)
13 | else
14 | conn
15 | end
16 | end
17 |
18 | defp check_password_state(conn) do
19 | user = current_user(conn)
20 | if user.password_initialized or allowed_path?(conn) do
21 | conn
22 | else
23 | redirect(conn, to: Opencov.Router.Helpers.profile_path(conn, :edit_password)) |> halt
24 | end
25 | end
26 |
27 | defp allowed_path?(conn) do
28 | conn.request_path in [
29 | Opencov.Router.Helpers.profile_path(conn, :edit_password),
30 | Opencov.Router.Helpers.profile_path(conn, :update_password),
31 | Opencov.Router.Helpers.auth_path(conn, :logout)
32 | ]
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/opencov.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov do
2 | use Application
3 |
4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec, warn: false
8 |
9 | children = [
10 | # Start the endpoint when the application starts
11 | {Opencov.Endpoint, []},
12 | # Start the Ecto repository
13 | {Opencov.Repo, []},
14 |
15 | {Phoenix.PubSub, [name: Opencov.PubSub, adapter: Phoenix.PubSub.PG2]}
16 | ]
17 |
18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
19 | # for other strategies and supported options
20 | opts = [strategy: :one_for_one, name: Opencov.Supervisor]
21 | Supervisor.start_link(children, opts)
22 | end
23 |
24 | # Tell Phoenix to update the endpoint configuration
25 | # whenever the application is updated.
26 | def config_change(changed, _new, removed) do
27 | Opencov.Endpoint.config_change(changed, removed)
28 | :ok
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/web/templates/admin/project/show.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Admin", to: admin_dashboard_path(@conn, :index) %>
5 | >
6 | <%= link "Projects", to: admin_project_path(@conn, :index) %>
7 | >
8 | <%= @project.name %>
9 |
10 |
11 |
12 |
13 | Name
14 | <%= @project.name %>
15 | URL
16 | <%= link @project.base_url, to: @project.base_url, target: "_blank" %>
17 | Token
18 | <%= @project.token %>
19 | Created on
20 | <%= Datetime.format(@project.inserted_at, :dateonly) %>
21 |
22 |
23 | <%= render Opencov.Admin.SharedView, "actions.html", conn: @conn, resource: @project, path_fn: &Opencov.Router.Helpers.project_path/3 %>
24 |
25 |
--------------------------------------------------------------------------------
/priv/repo/migrations/20151022141210_create_build.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Repo.Migrations.CreateBuild do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:builds) do
6 | add :build_number, :integer, null: false
7 | add :project_id, :integer, null: false
8 | add :coverage, :float, null: false
9 | add :previous_coverage, :float
10 | add :previous_build_id, :integer
11 |
12 | add :service_name, :string
13 | add :service_job_id, :string
14 | add :service_job_pull_request, :string
15 |
16 | add :commit_sha, :string
17 | add :committer_name, :string
18 | add :committer_email, :string
19 | add :commit_message, :text
20 | add :branch, :string, null: false
21 |
22 | add :build_started_at, :utc_datetime, null: false
23 | add :completed, :boolean
24 |
25 | timestamps()
26 | end
27 |
28 | create index(:builds, [:project_id])
29 | create unique_index(:builds, [:project_id, :build_number])
30 | create index(:builds, [:previous_build_id])
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/models/badge_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BadgeTest do
2 | use Opencov.ModelCase
3 |
4 | alias Opencov.Badge
5 |
6 | setup do
7 | svg_badge = insert(:badge, format: "svg") |> Repo.preload(:project)
8 | png_badge = insert(:badge, format: "png") |> Repo.preload(:project)
9 | {:ok, svg_badge: svg_badge, png_badge: png_badge}
10 | end
11 |
12 | test "default_format/0" do
13 | assert Badge.default_format == "svg"
14 | end
15 |
16 | test "for_project/2", %{svg_badge: svg_badge, png_badge: png_badge} do
17 | badge = Badge |> Badge.for_project(svg_badge.project) |> Repo.first!
18 | assert badge.id == svg_badge.id
19 |
20 | badge = Badge |> Badge.for_project(png_badge.project.id) |> Repo.first!
21 | assert badge.id == png_badge.id
22 | end
23 |
24 | test "with_format/2", %{svg_badge: svg_badge, png_badge: png_badge} do
25 | badge = Badge |> Badge.with_format("svg") |> Repo.first!
26 | assert badge.id == svg_badge.id
27 |
28 | badge = Badge |> Badge.with_format(:png) |> Repo.first!
29 | assert badge.id == png_badge.id
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/web/controllers/api/v1/job_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Api.V1.JobController do
2 | use Opencov.Web, :controller
3 |
4 | alias Opencov.ProjectManager
5 |
6 | def create(conn, %{"json" => json}) do
7 | json = Jason.decode!(json)
8 | handle_create(conn, json)
9 | end
10 |
11 | def create(conn, %{"json_file" => %Plug.Upload{path: filepath}}) do
12 | json = filepath |> File.read! |> Jason.decode!
13 | handle_create(conn, json)
14 | end
15 |
16 | def create(conn, _) do
17 | conn |> bad_request("request should have 'json' or 'json_file' parameter")
18 | end
19 |
20 | defp handle_create(conn, %{"repo_token" => token} = params) do
21 | project = ProjectManager.find_by_token!(token)
22 | {:ok, {_, job}} = ProjectManager.add_job!(project, params)
23 | render conn, "show.json", job: job
24 | end
25 |
26 | defp handle_create(conn, _) do
27 | conn |> bad_request("missing 'repo_token' parameter")
28 | end
29 |
30 | defp bad_request(conn, message) do
31 | conn
32 | |> put_status(400)
33 | |> json(%{"error" => message})
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/web/static/css/pages/project.styl:
--------------------------------------------------------------------------------
1 | .projects-list
2 | .projects
3 | margin-top 2em
4 |
5 | .project
6 | display block
7 | list-style-type none
8 | border-bottom 1px solid #ccc
9 | &:last-child
10 | border-bottom none
11 |
12 | display block
13 | width 100%
14 | height 100%
15 | padding 1em
16 |
17 | .content
18 | float left
19 | h3
20 | margin 0.5em 0 1em 0
21 |
22 | .coverage
23 | float right
24 | margin-right 1em
25 | font-size 1.6em
26 | line-height 2em
27 |
28 | .single-project
29 | .info
30 | font-size 1.2em
31 | padding 1em 2em
32 | max-height 3em
33 | .token-wrapper
34 | padding 0
35 | .badges
36 | width 100%
37 | .badge-list
38 | padding 0.5em 1em
39 | margin-bottom 0
40 | dd
41 | margin-bottom 1em
42 | &:last-child
43 | margin-bottom 0
44 | .builds
45 | td, th
46 | &:first-child
47 | width 50px
48 | &:nth-child(3), &:nth-child(4)
49 | width 75px
50 |
--------------------------------------------------------------------------------
/web/mailers/app_mailer.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.AppMailer do
2 | def send(email) do
3 | email = %{email | from: sender()}
4 | message = generate_message(email)
5 | Mailman.Adapter.deliver(mailman_config(), normalize_email(email), message)
6 | end
7 |
8 | defp generate_message(email) do
9 | Mailman.Render.render(email, %Mailman.EexComposeConfig{})
10 | end
11 |
12 | defp normalize_email(email) do
13 | %{email | from: extract_address(email.from),
14 | to: Enum.map(email.to, &extract_address/1)}
15 | end
16 |
17 | defp sender() do
18 | mail_config()[:sender]
19 | end
20 |
21 | defp mail_config do
22 | Application.get_env(:opencov, :email, [])
23 | end
24 |
25 | defp mailman_config() do
26 | if Mix.env == :test do
27 | %Mailman.TestConfig{}
28 | else
29 | struct(Mailman.SmtpConfig, mail_config()[:smtp])
30 | end
31 | end
32 |
33 | defp extract_address(email) do
34 | case Regex.run(~r/.*?<(.*?)>/, email) do
35 | [_, extracted] -> extracted |> String.trim() |> String.downcase()
36 | _ -> email
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Daniel Perez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/web/templates/mailers/user/confirmation.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= if opts[:registration] do %>
7 | Welcome to Opencov
8 | <% else %>
9 | Please confirm your email
10 | <% end %>
11 |
12 |
17 |
18 |
19 |
20 | Hi <%= user.name %>,
21 |
22 | <%= if opts[:registration] do %>
23 |
24 | <%= if opts[:invited?] do %>
25 | You have been invited to join
26 | <% else %>
27 | Thank you for registering to
28 | <% end %>
29 | Opencov@<%= base_url %> .
30 |
31 |
32 | Your login address is:
33 | <%= user.unconfirmed_email %>
34 |
35 | <% end %>
36 |
37 |
38 | <%= if opts[:invited?] do %>
39 | Please click here to set your password.
40 | <% else %>
41 | Please click here to confirm your email.
42 | <% end %>
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/eyecatch.rb:
--------------------------------------------------------------------------------
1 | before_build do
2 | run 'locale-gen en_US.UTF-8'
3 | run 'wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb'
4 | run 'dpkg -i erlang-solutions_1.0_all.deb'
5 | run 'apt-get update'
6 | run 'apt-get install -y elixir erlang-dev erlang-parsetools'
7 | run 'mix local.hex --force'
8 | run 'mix local.rebar --force'
9 | run 'npm install'
10 | run 'mix deps.get'
11 | run 'mix compile'
12 | run 'mix ecto.create'
13 | run 'mix ecto.migrate'
14 | run 'mix seedex.seed --env=eyecatch'
15 | end
16 |
17 | service 'postgresql'
18 |
19 | serve 'mix phoenix.server'
20 | port 4000
21 |
22 | window_width 1200
23 |
24 | start_delay 10
25 |
26 | task('anonymous') do
27 | entry_point '/'
28 | end
29 |
30 | task('user') do
31 | before_capture('/login') do
32 | fill_in 'login[email]', with: 'admin@example.com'
33 | fill_in 'login[password]', with: 'p4ssw0rd'
34 | click_button 'Login'
35 | end
36 | entry_point '/login'
37 | exclude_paths [%r{/builds/\d+?.+}, %r{/jobs/\d+?.+}]
38 | end
39 |
40 | env 'MIX_ENV=dev'
41 | env 'LANG=en_US.UTF-8'
42 | env 'LANGUAGE=en_US:en'
43 | env 'LC_ALL=en_US.UTF-8'
44 |
--------------------------------------------------------------------------------
/web/managers/settings_manager.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.SettingsManager do
2 | use Opencov.Web, :manager
3 |
4 | @required_fields ~w()a
5 | @optional_fields ~w(restricted_signup_domains signup_enabled default_project_visibility)a
6 |
7 | def changeset(model, params \\ :invalid) do
8 | model
9 | |> cast(params, @required_fields ++ @optional_fields)
10 | |> validate_required(@required_fields)
11 | |> validate_inclusion(:default_project_visibility, Opencov.Project.visibility_choices)
12 | |> normalize_domains
13 | end
14 |
15 | defp normalize_domains(changeset) do
16 | if domains = get_change(changeset, :restricted_signup_domains) do
17 | put_change(changeset, :restricted_signup_domains, String.trim(domains))
18 | else
19 | changeset
20 | end
21 | end
22 |
23 | def get!() do
24 | # TODO: cache the value
25 | Opencov.Repo.first!(Opencov.Settings)
26 | end
27 |
28 | def restricted_signup_domains do
29 | domains = get!().restricted_signup_domains
30 | |> String.split
31 | |> Enum.filter(&(String.contains?(&1, ".")))
32 | if Enum.count(domains) > 0, do: domains, else: nil
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/web/templates/user/form.html.eex:
--------------------------------------------------------------------------------
1 | <%= form_for @changeset, @action, [class: "full-page"], fn f -> %>
2 | <%= if @changeset.action do %>
3 |
4 |
Could not save user, please check the errors below
5 |
6 | <% end %>
7 |
8 | <%= input(f, :name) %>
9 | <%= input(f, :email, attrs: [value: @changeset.data.email || @changeset.data.unconfirmed_email]) %>
10 |
11 | <%= if @show_password do %>
12 | <%= input(f, :password, type: :password_input) %>
13 | <%= input(f, :password_confirmation, type: :password_input) %>
14 | <% end %>
15 |
16 | <%= if @is_admin do %>
17 | <%= unless @show_password do %>
18 |
19 | An email will be sent and the user will enter his password on his first login.
20 |
21 | <% end %>
22 |
23 | <%= label f, :admin, class: "control-label" do %>
24 | <%= checkbox f, :admin %> Admin
25 | <% end %>
26 |
27 | <% end %>
28 |
29 |
30 | <%= submit "Save", class: "btn btn-primary" %>
31 |
32 | <% end %>
33 |
--------------------------------------------------------------------------------
/web/static/js/components/file-coverage/file-coverage.styl:
--------------------------------------------------------------------------------
1 | file-coverage
2 | .table.source
3 | caption
4 | padding-left 4em
5 | tbody
6 | background-color #fafaf0
7 | border-radius 5px
8 | tr
9 | height 1.5em
10 | &.covered
11 | background-color rgba(95,151,68,0.2)
12 | &.missed
13 | background-color rgba(185,73,71,0.3)
14 | td
15 | border-top none
16 | margin 0
17 | padding 0 0.5em
18 | vertical-align middle
19 | &.num
20 | padding 0 0 0 0.5em
21 | font-size 0.8em
22 | border-right 1px solid #ccc
23 | &.code
24 | pre
25 | background-color inherit
26 | padding 0
27 | border none
28 | margin 0
29 | &.hits
30 | font-size 0.8em
31 | text-align right
32 | span
33 | border-radius 5px
34 | padding 0 0.5em
35 | &.hits-number
36 | background-color rgba(95,151,68,0.4)
37 | &.missed-sign
38 | background-color rgba(185,73,71,0.5)
39 |
40 |
--------------------------------------------------------------------------------
/test/managers/user_manager_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.UserManagerTest do
2 | use Opencov.ManagerCase
3 |
4 | alias Opencov.User
5 | alias Opencov.UserManager
6 |
7 | test "changeset with valid attributes" do
8 | changeset = UserManager.changeset %User{}, Map.delete(params_for(:user), :password_confirmation)
9 | assert changeset.valid?
10 | end
11 |
12 | test "changeset with invalid attributes" do
13 | changeset = UserManager.changeset(%User{}, %{})
14 | refute changeset.valid?
15 | end
16 |
17 | test "password_update_changeset when user do not have a password" do
18 | password = "password123"
19 | user = insert(:user, password_initialized: false)
20 | changeset = UserManager.password_update_changeset(user, %{password: password, password_confirmation: password})
21 | assert changeset.valid?
22 | end
23 |
24 | test "password_update_changeset checks current_password when user has a password" do
25 | {old_password, password} = {"old_password123" ,"password123"}
26 | user = insert(:user, password_initialized: true, password: old_password)
27 | changeset = UserManager.password_update_changeset(user, %{password: password, password_confirmation: password})
28 | refute changeset.valid?
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/web/templates/admin/user/show.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Admin", to: admin_dashboard_path(@conn, :index) %>
5 | >
6 | <%= link "Users", to: admin_user_path(@conn, :index) %>
7 | >
8 | <%= @user.name %>
9 |
10 |
11 |
12 |
13 | Name
14 | <%= @user.name %>
15 | Email
16 | <%= @user.email %>
17 | Unconfirmed email
18 | <%= @user.unconfirmed_email %>
19 | Admin
20 | <%= Display.bool(@user.admin) %>
21 | Confirmed
22 | <%= Display.bool(!is_nil(@user.confirmed_at)) %>
23 | Password initialized
24 | <%= Display.bool(@user.password_initialized) %>
25 | Created on
26 | <%= Datetime.format(@user.inserted_at, :dateonly) %>
27 |
28 |
29 | <%= render Opencov.Admin.SharedView, "actions.html", conn: @conn, resource: @user, path_fn: &Opencov.Router.Helpers.admin_user_path/3 %>
30 |
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "opencov",
3 | "version": "0.1.0",
4 | "description": "Open sourced coverage tool",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "author": "Daniel Perez ",
13 | "license": "MIT",
14 | "devDependencies": {
15 | "babel-core": "^6.21.0",
16 | "babel-loader": "^7.1.0",
17 | "babel-plugin-transform-runtime": "^6.15.0",
18 | "babel-preset-es2015": "^6.18.0",
19 | "babel-runtime": "^6.20.0",
20 | "css-loader": "^0.28.9",
21 | "extract-text-webpack-plugin": "^3.0.2",
22 | "file-loader": "^1.1.6",
23 | "pug": "^2.0.0-rc.4",
24 | "pug-loader": "^2.3.0",
25 | "json-loader": "^0.5.3",
26 | "less": "^2.5.3",
27 | "less-loader": "^4.0.5",
28 | "style-loader": "^0.19.0",
29 | "stylus": "^0.54.5",
30 | "stylus-loader": "^3.0.1",
31 | "url-loader": "^0.6.2",
32 | "webpack": "^3.10.0"
33 | },
34 | "dependencies": {
35 | "bootstrap": "^3.3.5",
36 | "font-awesome": "^4.4.0",
37 | "highlight.js": "^9.12.0",
38 | "jquery": "^3.0.0",
39 | "lodash": "^4.17.4",
40 | "nib": "^1.1.0",
41 | "riot": "^2.2.4"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/web/templates/project/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 | Projects
4 |
5 | <%= link to: project_path(@conn, :new), class: "btn btn-primary media-right" do %>
6 |
7 | Add project
8 | <% end %>
9 |
10 |
11 |
12 | <%= for project <- @projects do %>
13 |
14 |
15 |
16 |
<%= link project.name, to: project_path(@conn, :show, project) %>
17 |
18 |
19 | <%= format_coverage(project.current_coverage) %>
20 |
21 |
22 | <% latest_build = List.first(project.builds) %>
23 | <%= if latest_build && latest_build.commit_message do %>
24 | <%= link latest_build.commit_message, to: build_path(@conn, :show, latest_build) %>
25 | <%= if latest_build.branch do %>
26 | on branch <%= latest_build.branch %>
27 | <% end %>
28 | <%= human_time_ago(latest_build.inserted_at) %>
29 | <% end %>
30 |
31 | <% end %>
32 |
33 |
34 |
--------------------------------------------------------------------------------
/test/managers/badge_manager_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BadgeManagerTest do
2 | use Opencov.ModelCase
3 |
4 | alias Opencov.BadgeManager
5 |
6 | @format :svg
7 |
8 | setup do
9 | project = insert(:project, current_coverage: 58.4)
10 | {:ok, project: project}
11 | end
12 |
13 | test "get_or_create when no badge exist", %{project: project} do
14 | {:ok, badge} = BadgeManager.get_or_create(project, @format)
15 | assert badge.id
16 | assert badge.coverage == project.current_coverage
17 | end
18 |
19 | test "get_or_create when badge exists", %{project: project} do
20 | {:ok, badge} = BadgeManager.get_or_create(project, @format)
21 | assert badge.id
22 |
23 | {:ok, new_badge} = BadgeManager.get_or_create(project, @format)
24 | assert badge.id == new_badge.id
25 | end
26 |
27 | test "get_or_create when badge exists and coverage changed", %{project: project} do
28 | {:ok, badge} = BadgeManager.get_or_create(project, @format)
29 | assert badge.id
30 |
31 | new_coverage = 62.4
32 | project = Repo.update!(Ecto.Changeset.change(project, current_coverage: new_coverage))
33 |
34 | {:ok, new_badge} = BadgeManager.get_or_create(project, @format)
35 | assert badge.id == new_badge.id
36 | assert new_badge.coverage == new_coverage
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/priv/repo/seeds/eyecatch/0011_builds.exs:
--------------------------------------------------------------------------------
1 | import Seedex
2 |
3 | seed Opencov.Build, fn build ->
4 | build
5 | |> Map.put(:id, 1)
6 | |> Map.put(:project_id, 1)
7 | |> Map.put(:branch, "master")
8 | |> Map.put(:commit_sha, "fec556ae8fa374591b0bb9c91be987e96e8c94c2")
9 | |> Map.put(:commit_message, "Run npm install in eyecatch.rb")
10 | |> Map.put(:committer_name, "Daniel Perez")
11 | |> Map.put(:committer_email, "tuvistavie@gmail.com")
12 | |> Map.put(:completed, true)
13 | |> Map.put(:coverage, 72.83)
14 | |> Map.put(:build_started_at, DateTime.utc_now())
15 | |> Map.put(:service_name, "travis")
16 | |> Map.put(:build_number, 1)
17 | end
18 |
19 | seed Opencov.Build, fn build ->
20 | build
21 | |> Map.put(:id, 2)
22 | |> Map.put(:project_id, 1)
23 | |> Map.put(:branch, "master")
24 | |> Map.put(:commit_sha, "7c466d40a5f864c0cd785299b126423d37252fe8")
25 | |> Map.put(:commit_message, "Update dependencies")
26 | |> Map.put(:committer_name, "Daniel Perez")
27 | |> Map.put(:committer_email, "tuvistavie@gmail.com")
28 | |> Map.put(:completed, true)
29 | |> Map.put(:coverage, 74.24)
30 | |> Map.put(:build_started_at, DateTime.utc_now())
31 | |> Map.put(:service_name, "travis")
32 | |> Map.put(:build_number, 2)
33 | |> Map.put(:previous_build_id, 1)
34 | |> Map.put(:previous_coverage, 72.83)
35 | end
36 |
--------------------------------------------------------------------------------
/web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | OpenCov
11 | ">
12 | ">
13 | ">
14 |
15 |
16 |
17 |
18 |
19 | <%= render "header.html", conn: @conn %>
20 |
21 |
22 | <%= if get_flash(@conn, :info) do %>
23 |
<%= get_flash(@conn, :info) %>
24 | <% end %>
25 | <%= if get_flash(@conn, :error) do %>
26 |
<%= get_flash(@conn, :error) %>
27 | <% end %>
28 |
29 |
30 |
31 | <%= @inner_content %>
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/web/mailers/user_mailer.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.UserMailer do
2 | use Opencov.Web, :mailer
3 |
4 | define_templates :confirmation, [:user, :base_url, :confirmation_url, :opts]
5 | define_templates :reset_password, [:user, :reset_password_url]
6 |
7 | def confirmation_email(user, opts \\ []) do
8 | confirmation_url = confirmation_url(user.confirmation_token)
9 | subject = if opts[:registration], do: "Welcome to Opencov", else: "Please confirm your email"
10 | %Mailman.Email{
11 | subject: subject,
12 | to: ["#{user.name} <#{user.unconfirmed_email}>"],
13 | text: confirmation_text(user, Opencov.Endpoint.url, confirmation_url, opts),
14 | html: confirmation_html(user, Opencov.Endpoint.url, confirmation_url, opts)
15 | }
16 | end
17 |
18 | defp confirmation_url(token),
19 | do: Opencov.Router.Helpers.user_url(Opencov.Endpoint, :confirm, token: token)
20 |
21 | def reset_password_email(user) do
22 | reset_password_url = reset_password_url(user.password_reset_token)
23 | %Mailman.Email{
24 | subject: "Reset your password",
25 | to: ["#{user.name} <#{user.email}>"],
26 | text: reset_password_text(user, reset_password_url),
27 | html: reset_password_html(user, reset_password_url)
28 | }
29 | end
30 |
31 | defp reset_password_url(token),
32 | do: Opencov.Router.Helpers.profile_url(Opencov.Endpoint, :reset_password, token: token)
33 | end
34 |
--------------------------------------------------------------------------------
/web/views/form_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.FormHelpers do
2 | import Opencov.ErrorHelpers
3 | import Phoenix.HTML.Tag
4 | import Phoenix.HTML.Form
5 |
6 | @input_default_opts [label: [], type: :text_input, attrs: [], args: []]
7 |
8 | def form_group(form, field, do: block) do
9 | content_tag :div, block, [class: "form-group #{state_class(form, field)}"]
10 | end
11 |
12 | def input(form, field, opts \\ []) do
13 | opts = Keyword.merge(@input_default_opts, opts)
14 | form_group form, field do
15 | [make_label_tag(form, field, opts),
16 | make_input_tag(form, field, opts),
17 | error_tag(form, field)] |> Enum.reject(&is_nil/1)
18 | end
19 | end
20 |
21 | defp make_label_tag(form, field, opts) do
22 | scope = to_string(opts[:scope] || form.name)
23 | text = Gettext.dgettext(Opencov.Gettext, scope, to_string(field))
24 | label(form, field, text, add_class(opts[:label], "form-label"))
25 | end
26 |
27 | defp make_input_tag(form, field, opts) do
28 | {mod, fun} = case opts[:type] do
29 | {_mod, _fun} = type -> type
30 | fun -> {Phoenix.HTML.Form, fun}
31 | end
32 | args = [form, field] ++ opts[:args] ++ [add_class(opts[:attrs], "form-control")]
33 | apply(mod, fun, args)
34 | end
35 |
36 | defp add_class(opts, class) do
37 | base_class = Keyword.get(opts, :class, "")
38 | Keyword.put(opts, :class, base_class <> " " <> class)
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/web/controllers/auth_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.AuthController do
2 | use Opencov.Web, :controller
3 |
4 | alias Opencov.Authentication
5 | alias Opencov.User
6 | alias Opencov.Repo
7 |
8 | def login(conn, _params) do
9 | render(conn, "login.html", email: "", error: nil, can_signup: can_signup?())
10 | end
11 |
12 | def make_login(conn, %{"login" => %{"email" => email, "password" => password}}) do
13 | if user = User.authenticate(Repo.get_by(User, email: email), password) do
14 | login_if_confirmed(conn, user)
15 | else
16 | render(conn, "login.html", email: email, error: "Wrong email or password", can_signup: can_signup?())
17 | end
18 | end
19 |
20 | def make_login(conn, _params) do
21 | render(conn, "login.html", email: "", error: "You need to provide your email and password")
22 | end
23 |
24 | defp login_if_confirmed(conn, user) do
25 | if is_nil(user.confirmed_at) do
26 | render(conn, "login.html", email: user.email, error: "Please confirm your email", can_signup: can_signup?())
27 | else
28 | conn |> Authentication.login(user) |> redirect(to: NavigationHistory.last_path(conn, default: "/"))
29 | end
30 | end
31 |
32 | defp can_signup?() do
33 | Opencov.SettingsManager.get!.signup_enabled
34 | end
35 |
36 | def logout(conn, _params) do
37 | conn
38 | |> Authentication.logout
39 | |> redirect(to: auth_path(conn, :login))
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/managers/file_manager_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.FileManagerTest do
2 | use Opencov.ModelCase
3 |
4 | alias Opencov.File
5 | alias Opencov.FileManager
6 |
7 | @coverage_lines [0, nil, 3, nil, 0, 1]
8 |
9 | test "changeset with valid attributes" do
10 | changeset = FileManager.changeset(%File{}, Map.put(params_for(:file), :job_id, 1))
11 | assert changeset.valid?
12 | end
13 |
14 | test "changeset with invalid attributes" do
15 | changeset = FileManager.changeset(%File{}, %{})
16 | refute changeset.valid?
17 | end
18 |
19 | test "empty coverage" do
20 | file = insert(:file, coverage_lines: [])
21 | assert file.coverage == 0
22 | end
23 |
24 | test "normal coverage" do
25 | file = insert(:file, coverage_lines: @coverage_lines)
26 | assert file.coverage == 50
27 | end
28 |
29 | test "set_previous_file when a previous file exists" do
30 | project = insert(:project)
31 | previous_job = insert(:job, job_number: 1, build: insert(:build, project: project, build_number: 1))
32 | job = insert(:job, job_number: 1, build: insert(:build, project: project, build_number: 2))
33 | assert job.previous_job_id == previous_job.id
34 |
35 | previous_file = insert(:file, job: previous_job, name: "file")
36 | file = insert(:file, job: job, name: "file")
37 | assert file.previous_file_id == previous_file.id
38 | assert file.previous_coverage == previous_file.coverage
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/web/views/common_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.CommonView do
2 | def format_coverage(num) when is_float(num), do: "#{Decimal.round(Decimal.from_float(num), 2)}%"
3 | def format_coverage(_), do: "NA"
4 |
5 | def coverage_color(coverage) do
6 | cond do
7 | is_nil(coverage) -> "na"
8 | coverage == 0 -> "none"
9 | coverage < 80 -> "low"
10 | coverage < 90 -> "normal"
11 | coverage < 100 -> "good"
12 | true -> "great"
13 | end
14 | end
15 |
16 | def human_time_ago(datetime) do
17 | "about " <> Timex.from_now(datetime)
18 | end
19 |
20 | def coverage_diff(previous, current) do
21 | formatted_diff = abs(current - previous) |> format_coverage
22 | cond do
23 | previous == current -> "Coverage has not changed."
24 | previous > current -> "Coverage has decreased by #{formatted_diff}."
25 | previous < current -> "Coverage has increased by #{formatted_diff}."
26 | end
27 | end
28 |
29 | def repository_class(project) do
30 | url = project.base_url
31 | cond do
32 | String.contains?(url, "github.com") -> "fa-github"
33 | String.contains?(url, "bitbucket.org") -> "fa-bitbucket"
34 | true -> "fa-database"
35 | end
36 | end
37 |
38 | def commit_link(project, sha) do
39 | url = project.base_url
40 | cond do
41 | String.contains?(url, "bitbucket.org") -> "#{url}/commits/#{sha}"
42 | true -> "#{url}/commit/#{sha}"
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/priv/repo/seeds/eyecatch/0013_files.exs:
--------------------------------------------------------------------------------
1 | import Seedex
2 |
3 | source = """
4 | defmodule Opencov.Plug.ForcePasswordInitialize do
5 | import Opencov.Helpers.Authentication
6 | import Plug.Conn, only: [halt: 1]
7 | import Phoenix.Controller, only: [redirect: 2]
8 |
9 | def init(opts) do
10 | opts
11 | end
12 |
13 | def call(conn, _opts) do
14 | if user_signed_in?(conn) do
15 | check_password_state(conn)
16 | else
17 | conn
18 | end
19 | end
20 |
21 | defp check_password_state(conn) do
22 | user = current_user(conn)
23 | if user.password_initialized or allowed_path?(conn) do
24 | conn
25 | else
26 | redirect(conn, to: Opencov.Router.Helpers.profile_path(conn, :edit_password)) |> halt
27 | end
28 | end
29 |
30 | defp allowed_path?(conn) do
31 | conn.request_path in [
32 | Opencov.Router.Helpers.profile_path(conn, :edit_password),
33 | Opencov.Router.Helpers.profile_path(conn, :update_password),
34 | Opencov.Router.Helpers.auth_path(conn, :logout)
35 | ]
36 | end
37 | end
38 | """
39 |
40 | seed Opencov.File, fn file ->
41 | file
42 | |> Map.put(:id, 1)
43 | |> Map.put(:name, "lib/opencov/plug/force_password_initialize.ex")
44 | |> Map.put(:job_id, 1)
45 | |> Map.put(:coverage_lines, [nil,nil,nil,nil,nil,nil,0,nil,nil,nil,38,19,nil,19,nil,nil,nil,nil,19,19,19,nil,0,
46 | nil,nil,nil,nil,0,nil,nil,nil,nil,nil,nil])
47 | |> Map.put(:source, source)
48 | |> Map.put(:coverage, 66.7)
49 | end
50 |
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | imports other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | import Plug.Conn
22 | import Phoenix.ConnTest
23 |
24 | alias Opencov.Repo
25 | import Ecto.Query, only: [from: 2]
26 | import Opencov.Factory
27 |
28 | import Opencov.Router.Helpers
29 |
30 | # The default endpoint for testing
31 | @endpoint Opencov.Endpoint
32 |
33 | def with_login(conn) do
34 | password = "foobar123"
35 | user = build(:user)
36 | |> Opencov.Factory.confirmed_user
37 | |> Opencov.Factory.with_secure_password(password)
38 | |> Opencov.Repo.insert!
39 | post conn, auth_path(conn, :login, %{"login" => %{"email" => user.email, "password" => password}})
40 | end
41 | end
42 | end
43 |
44 | setup _tags do
45 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Opencov.Repo)
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :opencov, Opencov.Endpoint,
4 | url: [host: "localhost"],
5 | root: Path.dirname(__DIR__),
6 | secret_key_base: "tfYGCfFfu10pV8G5gtUJ1do3LDwnu+eWBfL1sNtK8+bEwo6gNzFQZtWkdNQVlt+V",
7 | render_errors: [accepts: ~w(html json)],
8 | pubsub_server: Opencov.PubSub
9 |
10 | config :opencov,
11 | badge_format: "svg",
12 | base_url: nil,
13 | ecto_repos: [Opencov.Repo]
14 |
15 | config :logger, :console,
16 | format: "$time $metadata[$level] $message\n",
17 | metadata: [:request_id]
18 |
19 | config :phoenix, :generators,
20 | migration: true,
21 | binary_id: false
22 |
23 | config :phoenix, :json_library, Jason
24 |
25 | config :scrivener_html,
26 | routes_helper: Opencov.Router.Helpers
27 |
28 | config :opencov, PlugBasicAuth,
29 | enable: false
30 |
31 | config :seedex, repo: Opencov.Repo
32 |
33 | config :opencov, :email,
34 | sender: "OpenCov ",
35 | smtp: [
36 | relay: "smtp.mailgun.org",
37 | username: System.get_env("SMTP_USER") || "info@opencov.com",
38 | password: System.get_env("SMTP_PASSWORD") || "I wouldn't share this",
39 | port: 587,
40 | ssl: false,
41 | tls: :always,
42 | auth: :always
43 | ]
44 |
45 | config :opencov, :demo,
46 | enabled: System.get_env("OPENCOV_DEMO") == "true",
47 | email: "user@opencov.com",
48 | password: "password123"
49 |
50 | import_config "#{Mix.env}.exs"
51 |
52 | local_config_path = Path.expand("local.exs", __DIR__)
53 | if File.exists?(local_config_path), do: import_config local_config_path
54 |
--------------------------------------------------------------------------------
/test/models/file_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.FileTest do
2 | use Opencov.ModelCase
3 |
4 | alias Opencov.File
5 |
6 | test "for_job" do
7 | build = insert(:build)
8 | job = insert(:job, build: build)
9 | other_job = insert(:job, build: build)
10 | file = insert(:file, job: job)
11 | other_file = insert(:file, job: other_job)
12 |
13 | files_ids = Opencov.Repo.all(File.for_job(job)) |> Enum.map(fn f -> f.id end)
14 | other_files_ids = Opencov.Repo.all(File.for_job(other_job)) |> Enum.map(fn f -> f.id end)
15 |
16 | assert files_ids == [file.id]
17 | assert other_files_ids == [other_file.id]
18 | end
19 |
20 | test "covered and unperfect filters" do
21 | insert(:file, coverage_lines: [0, 0])
22 | insert(:file, coverage_lines: [100, 100])
23 | normal = insert(:file, coverage_lines: [50, 100, 0])
24 | normal_only = File |> File.with_filters(["unperfect", "covered"]) |> Opencov.Repo.all
25 | assert Enum.count(normal_only) == 1
26 | assert List.first(normal_only).id == normal.id
27 | end
28 |
29 | test "changed and cov_changed filters" do
30 | previous_file = insert(:file, source: "print 'hello'", coverage_lines: [0, 100]) |> Repo.preload(:job)
31 | file = insert(:file, coverage_lines: [0, 100], job: previous_file.job)
32 | cov_changed = File |> File.with_filters(["cov_changed"]) |> Opencov.Repo.all
33 | changed = File |> File.with_filters(["changed"]) |> Opencov.Repo.all
34 | refute file.id in Enum.map(cov_changed, &(&1.id))
35 | assert file.id in Enum.map(changed, &(&1.id))
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/opencov/mailer.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Mailer do
2 | @templates_base_path Path.join(__DIR__, "../../web/templates/mailers")
3 |
4 | defmacro __using__(_opts) do
5 | quote do
6 | require EEx
7 | import Opencov.Mailer
8 | end
9 | end
10 |
11 | defmacro define_template(action, params, format) do
12 | quote bind_quoted: [action: action, params: params, format: format] do
13 | path = Opencov.Mailer.template_path(__MODULE__, action, format)
14 | EEx.function_from_file :defp, String.to_atom("#{action}_#{format}"), path, params
15 | end
16 | end
17 |
18 | defmacro define_html_template(action, params) do
19 | quote bind_quoted: [action: action, params: params] do
20 | define_template(action, params, "html")
21 | end
22 | end
23 |
24 | defmacro define_text_template(action, params) do
25 | quote bind_quoted: [action: action, params: params] do
26 | define_template(action, params, "text")
27 | end
28 | end
29 |
30 | defmacro define_templates(action, params) do
31 | quote bind_quoted: [action: action, params: params] do
32 | define_text_template(action, params)
33 | define_html_template(action, params)
34 | end
35 | end
36 |
37 | def template_path(module, action, format) do
38 | Path.join([@templates_base_path, module_path(module), "#{action}.#{format}.eex"])
39 | end
40 |
41 | defp module_path(module) do
42 | module
43 | |> Atom.to_string()
44 | |> String.replace(~r/^Elixir\.Opencov\./, "")
45 | |> String.replace(".", "/")
46 | |> Macro.underscore()
47 | |> String.replace(~r/_mailer$/, "")
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 | use Phoenix.HTML
6 |
7 | @doc """
8 | Generates tag for inlined form input errors.
9 | """
10 | def error_tag(form, field) do
11 | if error = form.errors[field] do
12 | content_tag :span, translate_error(error), class: "help-block"
13 | end
14 | end
15 |
16 | def state_class(form, field) do
17 | cond do
18 | # The form was not yet submitted
19 | !Map.get(form.source, :action) -> ""
20 | form.errors[field] -> "has-error"
21 | true -> "has-success"
22 | end
23 | end
24 |
25 | @doc """
26 | Translates an error message using gettext.
27 | """
28 | def translate_error({msg, opts}) do
29 | # Because error messages were defined within Ecto, we must
30 | # call the Gettext module passing our Gettext backend. We
31 | # also use the "errors" domain as translations are placed
32 | # in the errors.po file.
33 | # Ecto will pass the :count keyword if the error message is
34 | # meant to be pluralized.
35 | # On your own code and templates, depending on whether you
36 | # need the message to be pluralized or not, this could be
37 | # written simply as:
38 | #
39 | # dngettext "errors", "1 file", "%{count} files", count
40 | # dgettext "errors", "is invalid"
41 | #
42 | if count = opts[:count] do
43 | Gettext.dngettext(Opencov.Gettext, "errors", msg, msg, count, opts)
44 | else
45 | Gettext.dgettext(Opencov.Gettext, "errors", msg, opts)
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/web/templates/admin/settings/edit.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Admin", to: admin_dashboard_path(@conn, :index) %>
5 | >
6 | Settings
7 |
8 |
9 |
10 | <%= form_for @changeset, admin_settings_path(@conn, :update), [class: "full-page"], fn f -> %>
11 | <%= if @changeset.action do %>
12 |
13 |
Oops, something went wrong! Please check the errors below:
14 |
15 | <%= for {attr, message} <- f.errors do %>
16 | <%= humanize(attr) %> <%= message %>
17 | <% end %>
18 |
19 |
20 | <% end %>
21 |
22 |
Projects
23 |
24 | <%= input(f, :default_project_visibility, type: :select,
25 | args: [["Private": "private", "Internal": "internal", "Public": "public"]]) %>
26 |
27 |
28 |
29 |
Login
30 |
31 |
32 | <%= label f, :signup_enabled, class: "control-label" do %>
33 | <%= checkbox f, :signup_enabled %> Signup Enabled
34 | <% end %>
35 |
36 |
37 |
38 | <%= label f, :restricted_signup_domains, class: "control-label" do %>
39 | Restricted signup domains (Enter one domain per line)
40 | <% end %>
41 | <%= textarea f, :restricted_signup_domains, class: "form-control" %>
42 |
43 |
44 |
45 | <%= submit "Update settings", class: "btn btn-primary" %>
46 |
47 | <% end %>
48 |
49 |
--------------------------------------------------------------------------------
/test/managers/project_manager_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ProjectManagerTest do
2 | use Opencov.ManagerCase
3 |
4 | alias Opencov.Project
5 | alias Opencov.ProjectManager
6 |
7 | test "changeset with valid attributes" do
8 | changeset = ProjectManager.changeset(%Project{}, params_for(:project))
9 | assert changeset.valid?
10 | end
11 |
12 | test "changeset with invalid attributes" do
13 | changeset = ProjectManager.changeset(%Project{}, %{})
14 | refute changeset.valid?
15 | end
16 |
17 | test "find_by_token with existing token" do
18 | project = insert(:project)
19 | assert ProjectManager.find_by_token(project.token) == project
20 | end
21 |
22 | test "find_by_token with inexisting token" do
23 | assert ProjectManager.find_by_token("inexisting") == nil
24 | end
25 |
26 | test "find_by_token! with existing token" do
27 | project = insert(:project)
28 | assert ProjectManager.find_by_token!(project.token) == project
29 | end
30 |
31 | test "find_by_token! with inexisting token" do
32 | assert_raise Ecto.NoResultsError, fn -> ProjectManager.find_by_token!("inexisting") end
33 | end
34 |
35 | test "add_job!" do
36 | project = insert(:project)
37 | cov = Opencov.Fixtures.dummy_coverage
38 | {:ok, {build, job}} = ProjectManager.add_job!(project, cov)
39 | assert build.id
40 | assert job.id
41 | assert build.commit_sha == cov["git"]["head"]["id"]
42 | assert Enum.count(Repo.preload(job, :files).files) == Enum.count(cov["source_files"])
43 | assert Repo.get!(Opencov.Build, build.id).coverage == job.coverage
44 | assert Repo.get!(Opencov.Project, project.id).current_coverage == job.coverage
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/web/templates/admin/user/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Admin", to: admin_dashboard_path(@conn, :index) %>
5 | >
6 | Users
7 |
8 |
9 | <%= link "Create user", to: admin_user_path(@conn, :new), class: "btn btn-primary btn-sm pull-right" %>
10 |
11 |
12 |
13 |
14 | <%= @paginator.total_entries %> total users
15 |
16 |
17 | Name
18 | Email
19 | Confirmed
20 | Admin
21 |
22 |
23 |
24 |
25 | <%= for user <- @users do %>
26 |
27 | <%= link user.name, to: admin_user_path(@conn, :show, user) %>
28 | <%= user.email %>
29 | <%= Display.bool(!is_nil(user.confirmed_at)) %>
30 | <%= Display.bool(user.admin) %>
31 |
32 | <%= link "Edit", to: admin_user_path(@conn, :edit, user), class: "btn btn-default btn-xs" %>
33 | <%= link "Delete", to: admin_user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %>
34 |
35 |
36 | <% end %>
37 |
38 |
39 |
40 | <%= if @paginator.total_pages > 1 do %>
41 |
42 | <%= pagination_links @conn, @paginator, path: &Opencov.Router.Helpers.admin_user_path/3 %>
43 |
44 | <% end %>
45 |
46 |
--------------------------------------------------------------------------------
/web/templates/admin/project/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= link "Admin", to: admin_dashboard_path(@conn, :index) %>
5 | >
6 | Projects
7 |
8 |
9 | <%= link "Create project", to: project_path(@conn, :new), class: "btn btn-primary btn-sm pull-right" %>
10 |
11 |
12 |
13 |
14 | <%= @paginator.total_entries %> total projects
15 |
16 |
17 | Name
18 | URL
19 | Created on
20 |
21 |
22 |
23 |
24 | <%= for project <- @projects do %>
25 |
26 | <%= link project.name, to: admin_project_path(@conn, :show, project) %>
27 | <%= link project.base_url, to: project.base_url, target: "_blank" %>
28 | <%= Datetime.format(project.inserted_at, :dateonly) %>
29 |
30 | <%= link "Edit", to: project_path(@conn, :edit, project), class: "btn btn-default btn-xs" %>
31 | <%= link "Delete", to: project_path(@conn, :delete, project), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %>
32 |
33 |
34 | <% end %>
35 |
36 |
37 |
38 | <%= if @paginator.total_pages > 1 do %>
39 |
40 | <%= pagination_links @conn, @paginator, path: &Opencov.Router.Helpers.admin_project_path/3 %>
41 |
42 | <% end %>
43 |
44 |
--------------------------------------------------------------------------------
/test/managers/job_manager_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.JobManagerTest do
2 | use Opencov.ModelCase
3 |
4 | alias Opencov.Job
5 | alias Opencov.JobManager
6 |
7 | test "changeset with valid attributes" do
8 | changeset = JobManager.changeset(%Job{}, Map.put(params_for(:job), :build_id, 1))
9 | assert changeset.valid?
10 | end
11 |
12 | test "changeset with invalid attributes" do
13 | changeset = JobManager.changeset(%Job{}, %{})
14 | refute changeset.valid?
15 | end
16 |
17 | test "set_job_number" do
18 | previous_job = insert(:job) |> Repo.preload(:build)
19 | job = insert(:job, job_number: nil, build: previous_job.build)
20 | assert job.job_number == previous_job.job_number + 1
21 | end
22 |
23 | test "create_from_json!" do
24 | dummy_coverage = Opencov.Fixtures.dummy_coverage
25 | job = JobManager.create_from_json!(insert(:build), dummy_coverage)
26 | assert job.id != nil
27 | assert Enum.count(job.files) == Enum.count(dummy_coverage["source_files"])
28 | assert job.files_count == Enum.count(dummy_coverage["source_files"])
29 | assert job.coverage > 90
30 | end
31 |
32 | test "set_previous_values when no previous job exists" do
33 | job = insert(:job)
34 | assert job.previous_job_id == nil
35 | end
36 |
37 | test "set_previous_values when a previous job exists" do
38 | project = insert(:project)
39 | previous_job = insert(:job, job_number: 1, build: insert(:build, project: project, build_number: 1))
40 | job = insert(:job, job_number: 1, build: insert(:build, project: project, build_number: 2))
41 | assert job.previous_job_id == previous_job.id
42 | assert job.previous_coverage == previous_job.coverage
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/support/model_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ModelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | model tests.
5 |
6 | You may define functions here to be used as helpers in
7 | your model tests. See `errors_on/2`'s definition as reference.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | alias Opencov.Repo
20 | import Ecto.Query, only: [from: 2]
21 | import Opencov.Factory
22 | import Opencov.ModelCase
23 | end
24 | end
25 |
26 | setup _tags do
27 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Opencov.Repo)
28 | end
29 |
30 | @doc """
31 | Helper for returning list of errors in model when passed certain data.
32 |
33 | ## Examples
34 |
35 | Given a User model that lists `:name` as a required field and validates
36 | `:password` to be safe, it would return:
37 |
38 | iex> errors_on(%User{}, %{password: "password"})
39 | [password: "is unsafe", name: "is blank"]
40 |
41 | You could then write your assertion like:
42 |
43 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"})
44 |
45 | You can also create the changeset manually and retrieve the errors
46 | field directly:
47 |
48 | iex> changeset = UserManager.changeset(%User{}, password: "password")
49 | iex> {:password, "is unsafe"} in changeset.errors
50 | true
51 | """
52 | def errors_on(model, data) do
53 | model.__struct__.changeset(model, data).errors
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/web/managers/badge_manager.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BadgeManager do
2 | use Opencov.Web, :manager
3 |
4 | alias Opencov.Badge
5 | alias Opencov.BadgeCreator
6 |
7 | import Opencov.Badge
8 |
9 | @required_fields ~w(image format project_id)a
10 | @optional_fields ~w(coverage)a
11 |
12 | def changeset(model, params \\ :invalid) do
13 | model
14 | |> cast(params, @required_fields ++ @optional_fields)
15 | |> validate_required(@required_fields)
16 | end
17 |
18 | def get_or_create(project, format \\ default_format()) do
19 | case find(project.id, format) do
20 | nil -> create(project, format)
21 | badge -> return_or_update(project, badge)
22 | end
23 | end
24 |
25 | defp return_or_update(project, badge) do
26 | if project.current_coverage == badge.coverage,
27 | do: {:ok, badge},
28 | else: update(project, badge)
29 | end
30 |
31 | defp make(project, format, cb) do
32 | case BadgeCreator.make_badge(project.current_coverage, format: format) do
33 | {:ok, _format, image} -> {:ok, cb.(image)}
34 | {:error, e} -> {:error, e}
35 | end
36 | end
37 |
38 | defp create(project, format) do
39 | make project, format, fn image ->
40 | params = %{image: image, format: to_string(format), coverage: project.current_coverage}
41 | Ecto.build_assoc(project, :badge)
42 | |> changeset(params)
43 | |> Repo.insert!
44 | end
45 | end
46 |
47 | defp find(project, format),
48 | do: Badge |> for_project(project) |> with_format(format) |> Repo.first
49 |
50 | defp update(project, badge) do
51 | make project, badge.format, fn image ->
52 | changeset(badge, %{coverage: project.current_coverage, image: image})
53 | |> Repo.update!
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with brunch.io to recompile .js and .css sources.
9 | config :opencov, Opencov.Endpoint,
10 | http: [port: System.get_env("PORT") || 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | cache_static_lookup: false,
14 | check_origin: false,
15 | watchers: [
16 | {Path.expand("node_modules/webpack/bin/webpack.js"), [
17 | "--watch", "--colors", "--progress", cd: Path.expand("../", __DIR__)]}
18 | ]
19 |
20 | # Watch static and templates for browser reloading.
21 | config :opencov, Opencov.Endpoint,
22 | live_reload: [
23 | patterns: [
24 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
25 | ~r{priv/gettext/.*(po)$},
26 | ~r{web/views/.*(ex)$},
27 | ~r{web/templates/.*(eex)$}
28 | ]
29 | ]
30 |
31 | # Do not include metadata nor timestamps in development logs
32 | config :logger, :console, format: "[$level] $message\n"
33 |
34 | # Set a higher stacktrace during development.
35 | # Do not configure such in production as keeping
36 | # and calculating stacktraces is usually expensive.
37 | config :phoenix, :stacktrace_depth, 20
38 |
39 | # Configure your database
40 | config :opencov, Opencov.Repo,
41 | adapter: Ecto.Adapters.Postgres,
42 | username: "postgres",
43 | password: "postgres",
44 | database: "opencov_dev",
45 | hostname: "localhost",
46 | pool_size: 10
47 |
48 |
49 | config :opencov, :email,
50 | sender: "OpenCov ",
51 | smtp: [
52 | relay: "127.0.0.1",
53 | port: 1025,
54 | ssl: false,
55 | tls: :never,
56 | auth: :never
57 | ]
58 |
--------------------------------------------------------------------------------
/web/templates/layout/header.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 | <%= if user_signed_in?(@conn) do %>
10 |
11 | <%= if current_user(@conn).admin do %>
12 |
13 | <%= link to: admin_dashboard_path(@conn, :index) do %>
14 |
15 | Admin
16 | <% end %>
17 |
18 | <% end %>
19 |
20 |
21 |
22 | <%= current_user(@conn).name %>
23 |
24 |
47 |
48 |
49 | <% end %>
50 |
51 |
52 |
--------------------------------------------------------------------------------
/web/managers/file_manager.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.FileManager do
2 | use Opencov.Web, :manager
3 | import Opencov.File
4 | alias Opencov.File
5 |
6 | @required_fields ~w(name source coverage_lines)a
7 | @optional_fields ~w(job_id)a
8 |
9 | def changeset(model, params \\ :invalid) do
10 | model
11 | |> cast(normalize_params(params), @required_fields ++ @optional_fields)
12 | |> validate_required(@required_fields)
13 | |> generate_coverage
14 | |> prepare_changes(&set_previous_file/1)
15 | end
16 |
17 | defp normalize_params(%{"coverage" => coverage} = params) when is_list(coverage) do
18 | {lines, params} = Map.pop(params, "coverage")
19 | Map.put(params, "coverage_lines", lines)
20 | end
21 | defp normalize_params(params), do: params
22 |
23 | defp generate_coverage(changeset) do
24 | case get_change(changeset, :coverage_lines) do
25 | nil -> changeset
26 | lines -> put_change(changeset, :coverage, compute_coverage(lines))
27 | end
28 | end
29 |
30 | defp set_previous_file(changeset),
31 | do: set_previous_file(changeset, job_for_changeset(changeset))
32 | defp set_previous_file(changeset, %Opencov.Job{previous_job_id: previous_job_id})
33 | when not is_nil(previous_job_id) do
34 | file = find_previous_file(previous_job_id, changeset.changes.name)
35 | set_previous_file(changeset, file)
36 | end
37 | defp set_previous_file(changeset, %Opencov.File{id: id, coverage: coverage}),
38 | do: change(changeset, previous_file_id: id, previous_coverage: coverage)
39 | defp set_previous_file(changeset, _), do: changeset
40 |
41 | defp job_for_changeset(changeset) do
42 | job_id = get_change(changeset, :job_id) || changeset.data.job_id
43 | Repo.get(Opencov.Job, job_id)
44 | end
45 |
46 | defp find_previous_file(previous_job_id, name) do
47 | File |> for_job(previous_job_id) |> with_name(name) |> Opencov.Repo.first
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :opencov,
6 | version: "0.0.1",
7 | elixir: "~> 1.4",
8 | elixirc_paths: elixirc_paths(Mix.env),
9 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
10 | build_embedded: Mix.env == :prod,
11 | start_permanent: Mix.env == :prod,
12 | aliases: aliases(),
13 | test_coverage: [tool: ExCoveralls],
14 | deps: deps()]
15 | end
16 |
17 | def application do
18 | [mod: {Opencov, []},
19 | extra_applications: [:logger, :eex]]
20 | end
21 |
22 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
23 | defp elixirc_paths(_), do: ["lib", "web"]
24 |
25 | defp deps do
26 | [{:phoenix, "~> 1.2"},
27 | {:phoenix_ecto, "~> 3.2"},
28 | {:gen_smtp, "~> 1.1"},
29 | {:postgrex, "~> 0.13"},
30 | {:phoenix_html, "~> 2.6"},
31 | {:gettext, "~> 0.11"},
32 | {:cowboy, "~> 2.7"},
33 | {:plug_cowboy, "~> 2.0"},
34 | {:exgravatar, "~> 2.0"},
35 | {:secure_random, "~> 0.2"},
36 | {:temp, "~> 0.4"},
37 | {:timex, "~> 3.1"},
38 | {:timex_ecto, "~> 3.1"},
39 | {:scrivener_ecto, "~> 1.0"},
40 | {:basic_auth, "~> 2.0"},
41 | {:navigation_history, "~> 0.2"},
42 | {:ex_machina, "~> 2.0"},
43 | {:mailman, github: "mailman-elixir/mailman"},
44 | {:scrivener_html, "~> 1.3"},
45 | {:secure_password, "~> 0.4"},
46 | {:seedex, "~> 0.1.3"},
47 | {:jason, "~> 1.2"},
48 | {:phoenix_live_reload, "~> 1.0", only: :dev},
49 | {:mix_test_watch, "~> 0.2", only: :dev},
50 | {:excoveralls, "~> 0.6", only: :test},
51 | {:mock, "~> 0.3", only: :test}]
52 | end
53 |
54 | defp aliases do
55 | ["ecto.setup": ["ecto.create", "ecto.migrate", "seedex.seed"],
56 | "ecto.reset": ["ecto.drop", "ecto.setup"],
57 | "assets.compile": [&compile_assets/1, "phx.digest"]]
58 | end
59 |
60 | defp compile_assets(_) do
61 | System.cmd(Path.expand("node_modules/.bin/webpack", __DIR__), ["-p"], into: IO.stream(:stdio, :line))
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/test/views/common_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.CommonViewTest do
2 | use Opencov.ConnCase, async: true
3 |
4 | import Opencov.CommonView
5 |
6 | test "coverage_color" do
7 | assert coverage_color(nil) == "na"
8 | assert coverage_color(0) == "none"
9 | assert coverage_color(1) == "low"
10 | assert coverage_color(50) == "low"
11 | assert coverage_color(80) == "normal"
12 | assert coverage_color(85) == "normal"
13 | assert coverage_color(90) == "good"
14 | assert coverage_color(95) == "good"
15 | assert coverage_color(100) == "great"
16 | end
17 |
18 | test "human_time_ago" do
19 | date = Timex.shift(DateTime.utc_now(), minutes: -5)
20 | assert human_time_ago(date) == "about 5 minutes ago"
21 |
22 | date = Timex.shift(DateTime.utc_now(), days: -8)
23 | assert human_time_ago(date) == "about 8 days ago"
24 | end
25 |
26 | test "coverage_diff" do
27 | assert coverage_diff(60.0, 60.0) == "Coverage has not changed."
28 | assert coverage_diff(64.0, 60.0) == "Coverage has decreased by 4.0%."
29 | assert coverage_diff(60.0, 64.0) == "Coverage has increased by 4.0%."
30 | end
31 |
32 | test "repository_class" do
33 | assert repository_class(build(:project, base_url: "https://github.com/tuvistavie/opencov")) == "fa-github"
34 | assert repository_class(build(:project, base_url: "https://bitbucket.org/tuvistavie/opencov")) == "fa-bitbucket"
35 | assert repository_class(build(:project, base_url: "https://gitlab.com/tuvistavie/opencov")) == "fa-database"
36 | end
37 |
38 | test "commit_link" do
39 | project = build(:project, base_url: "https://github.com/tuvistavie/opencov")
40 | assert commit_link(project, "foobar") == "https://github.com/tuvistavie/opencov/commit/foobar"
41 | project = build(:project, base_url: "https://gitlab.com/tuvistavie/opencov")
42 | assert commit_link(project, "foobar") == "https://gitlab.com/tuvistavie/opencov/commit/foobar"
43 | project = build(:project, base_url: "https://bitbucket.org/tuvistavie/opencov")
44 | assert commit_link(project, "foobar") == "https://bitbucket.org/tuvistavie/opencov/commits/foobar"
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/web/managers/project_manager.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ProjectManager do
2 | use Opencov.Web, :manager
3 | alias Opencov.Project
4 | import Opencov.Project
5 |
6 | import Ecto.Query
7 |
8 | @required_fields ~w(name base_url)a
9 | @optional_fields ~w(token current_coverage)a
10 |
11 | def changeset(model, params \\ :invalid) do
12 | model |> edit_changeset(params) |> generate_token
13 | end
14 |
15 | def edit_changeset(model, params \\ :invalid) do
16 | model
17 | |> cast(params, @required_fields ++ @optional_fields)
18 | |> validate_required(@required_fields)
19 | end
20 |
21 | def generate_token(changeset) do
22 | put_change(changeset, :token, unique_token())
23 | end
24 |
25 | defp unique_token() do
26 | token = SecureRandom.urlsafe_base64(30)
27 | if find_by_token(token), do: unique_token(), else: token
28 | end
29 |
30 | def find_by_token(token) do
31 | with_token(Project, token) |> Repo.first
32 | end
33 |
34 | def find_by_token!(token) do
35 | with_token(Project, token) |> Repo.first!
36 | end
37 |
38 | def update_coverage(project) do
39 | coverage = (Opencov.Build.last_for_project(Opencov.Build, project) |> Repo.first!).coverage
40 | Repo.update! change(project, current_coverage: coverage)
41 | end
42 |
43 | def preload_latest_build(projects) do
44 | query = from b in Opencov.Build,
45 | join: p in assoc(b, :project),
46 | where: b.completed and b.id == fragment("""
47 | (SELECT id
48 | FROM builds AS b
49 | WHERE b.project_id = ?
50 | ORDER BY b.inserted_at
51 | DESC LIMIT 1)
52 | """, p.id),
53 | order_by: [desc: b.inserted_at]
54 | Opencov.Repo.preload(projects, builds: query)
55 | end
56 |
57 | def preload_recent_builds(projects) do
58 | query = from b in Opencov.Build, where: b.completed, order_by: [desc: b.inserted_at], limit: 10
59 | Opencov.Repo.preload(projects, builds: query)
60 | end
61 |
62 | def add_job!(project, params) do
63 | Opencov.Repo.transaction fn ->
64 | build = Opencov.BuildManager.get_or_create!(project, params)
65 | job = Opencov.JobManager.create_from_json!(build, params)
66 | {build, job}
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/web/templates/admin/dashboard/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Admin dashboard
4 |
5 |
6 |
7 |
8 |
9 | Recent projects
10 | <%= link "Create project", to: project_path(@conn, :new), class: "btn btn-primary btn-sm pull-right" %>
11 |
12 |
13 |
14 |
15 | <%= for project <- @projects do %>
16 | <%= link project.name, to: admin_project_path(@conn, :show, project) %>
17 | <% end %>
18 |
19 |
20 | <%= link "View all >>", to: admin_project_path(@conn, :index), class: "view-all" %>
21 |
22 |
23 |
24 |
25 |
26 | Recent users
27 | <%= link "Create user", to: admin_user_path(@conn, :new), class: "btn btn-primary btn-sm pull-right" %>
28 |
29 |
30 |
31 |
32 | <%= for user <- @users do %>
33 | <%= link user.name, to: admin_user_path(@conn, :show, user) %>
34 | <% end %>
35 |
36 |
37 | <%= link "View all >>", to: admin_user_path(@conn, :index), class: "view-all" %>
38 |
39 |
40 |
41 |
42 |
43 | Settings
44 | <%= link "Edit settings", to: admin_settings_path(@conn, :edit), class: "btn btn-primary btn-sm pull-right" %>
45 |
46 |
47 |
48 |
49 | <%= for key <- ~w(signup_enabled restricted_signup_domains default_project_visibility)a do %>
50 | <%= Display.atom(key) %>
51 | <%= Display.display(Map.get(@settings, key)) %>
52 | <% end %>
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/web/controllers/user_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.UserController do
2 | use Opencov.Web, :controller
3 |
4 | alias Opencov.User
5 | alias Opencov.UserManager
6 | import Opencov.Helpers.Authentication
7 |
8 | alias Opencov.UserService
9 |
10 | plug :scrub_params, "user" when action in [:create, :update]
11 | plug :check_signup when action in [:new, :create]
12 |
13 | def new(conn, _params) do
14 | changeset = UserManager.changeset(%User{})
15 | render(conn, "new.html", changeset: changeset)
16 | end
17 |
18 | def create(conn, %{"user" => user_params}) do
19 | case UserService.create_user(make_user_params(user_params), invited?: false) do
20 | {:ok, _user} ->
21 | conn
22 | |> put_flash(:info, "Please confirm your email address.")
23 | |> redirect(to: auth_path(conn, :login))
24 | {:error, changeset} ->
25 | render(conn, "new.html", changeset: changeset)
26 | end
27 | end
28 |
29 | def confirm(conn, %{"token" => token}) do
30 | case UserService.confirm_user(token) do
31 | {:ok, user, message} ->
32 | conn
33 | |> put_flash(:info, message)
34 | |> finalize_confirm(user)
35 | {:error, err} -> redirect_to_top_with_error(conn, err)
36 | end
37 | end
38 | def confirm(conn, _params),
39 | do: redirect_to_top_with_error(conn, "The URL seems wrong, double check your email")
40 |
41 | defp finalize_confirm(conn, user) do
42 | if user.password_initialized do
43 | conn |> redirect(to: auth_path(conn, :login))
44 | else
45 | conn |> Opencov.Authentication.login(user) |> redirect(to: profile_path(conn, :edit_password))
46 | end
47 | end
48 |
49 | defp redirect_to_top_with_error(conn, err) do
50 | redirect_path = if user_signed_in?(conn), do: "/", else: auth_path(conn, :login)
51 | conn |> put_flash(:error, err) |> redirect(to: redirect_path)
52 | end
53 |
54 | defp make_user_params(params) do
55 | params |> Map.delete("admin")
56 | end
57 |
58 | defp check_signup(conn, _) do
59 | if Opencov.SettingsManager.get!.signup_enabled do
60 | conn
61 | else
62 | conn
63 | |> put_flash(:info, "Signup is disabled. Contact your administrator if you need an account.")
64 | |> redirect(to: auth_path(conn, :login))
65 | |> halt
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/test/controllers/api/v1/job_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Api.V1.JobControllerTest do
2 | use Opencov.ConnCase
3 |
4 | setup do
5 | conn = build_conn() |> put_req_header("content-type", "application/json")
6 | {:ok, conn: conn}
7 | end
8 |
9 | test "returns 400 when data not sent", %{conn: conn} do
10 | conn = post conn, api_v1_job_path(conn, :create), ""
11 | assert json_response(conn, 400)
12 | end
13 |
14 | test "returns 400 when project token not given", %{conn: conn} do
15 | payload = Jason.encode!(%{json: Jason.encode!(Opencov.Fixtures.dummy_coverage)})
16 | conn = post conn, api_v1_job_path(conn, :create), payload
17 | assert json_response(conn, 400)
18 | end
19 |
20 | test "returns 404 when inexistent token given", %{conn: conn} do
21 | data = Map.put(Opencov.Fixtures.dummy_coverage, "repo_token", "i-dont-exist")
22 | payload = Jason.encode!(%{json: Jason.encode!(data)})
23 | assert_raise Ecto.NoResultsError, fn ->
24 | post conn, api_v1_job_path(conn, :create), payload
25 | end
26 | end
27 |
28 | test "creates job when project exists", %{conn: conn} do
29 | project = insert(:project)
30 | data = Map.put(Opencov.Fixtures.dummy_coverage, "repo_token", project.token)
31 | payload = Jason.encode!(%{json: Jason.encode!(data)})
32 | conn = post conn, api_v1_job_path(conn, :create), payload
33 | assert json_response(conn, 200)
34 | build = Opencov.Build.for_commit(project, data["git"]) |> Opencov.Repo.first
35 | assert build
36 | job = List.first(Opencov.Repo.preload(build, :jobs).jobs)
37 | assert job
38 | assert job.files_count == Enum.count(data["source_files"])
39 | end
40 |
41 | test "works with multipart data", %{conn: conn} do
42 | project = insert(:project)
43 | data = Map.put(Opencov.Fixtures.dummy_coverage, "repo_token", project.token)
44 | {:ok, file_path} = Temp.open %{prefix: "opencov", suffix: ".json"}, &IO.write(&1, Jason.encode!(data))
45 | upload = %Plug.Upload{path: file_path, filename: "coverage.json", content_type: "application/json"}
46 | conn = put_req_header(conn, "content-type", "multipart/form-data")
47 | conn = post conn, api_v1_job_path(conn, :create), %{json_file: upload}
48 | assert json_response(conn, 200)
49 | assert Opencov.Build.for_commit(project, data["git"]) |> Opencov.Repo.first
50 | File.rm! file_path
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/web/web.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Web do
2 | @moduledoc """
3 | A module that keeps using definitions for controllers,
4 | views and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use Opencov.Web, :controller
9 | use Opencov.Web, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below.
17 | """
18 |
19 | def model do
20 | quote do
21 | use Ecto.Schema
22 | use Timex.Ecto.Timestamps
23 | use Opencov.Core
24 |
25 | import Ecto.Changeset
26 | import Ecto.Query, only: [from: 1, from: 2]
27 | end
28 | end
29 |
30 | def manager do
31 | quote do
32 | alias Opencov.Repo
33 | use Opencov.Core
34 |
35 | import Ecto.Changeset
36 | end
37 | end
38 |
39 | def service do
40 | quote do
41 | alias Opencov.Repo
42 | end
43 | end
44 |
45 | def controller do
46 | quote do
47 | use Phoenix.Controller
48 |
49 | alias Opencov.Repo
50 | import Ecto.Query, only: [from: 1, from: 2]
51 |
52 | import Opencov.Router.Helpers
53 | end
54 | end
55 |
56 | def view do
57 | quote do
58 | use Phoenix.View, root: "web/templates"
59 |
60 | # Import convenience functions from controllers
61 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
62 |
63 | # Use all HTML functionality (forms, tags, etc)
64 | use Phoenix.HTML
65 |
66 | import Opencov.Router.Helpers
67 | import Opencov.ErrorHelpers
68 | import Opencov.FormHelpers
69 | end
70 | end
71 |
72 | def router do
73 | quote do
74 | use Phoenix.Router
75 | end
76 | end
77 |
78 | def channel do
79 | quote do
80 | use Phoenix.Channel
81 |
82 | alias Opencov.Repo
83 | import Ecto.Query, only: [from: 1, from: 2]
84 | end
85 | end
86 |
87 | def mailer do
88 | quote do
89 | use Opencov.Mailer
90 | end
91 | end
92 |
93 | @doc """
94 | When used, dispatch to the appropriate controller/view/etc.
95 | """
96 | defmacro __using__(which) when is_atom(which) do
97 | apply(__MODULE__, which, [])
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/test/managers/build_manager_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BuildManagerTest do
2 | use Opencov.ManagerCase
3 |
4 | alias Opencov.Build
5 | alias Opencov.BuildManager
6 |
7 | test "changeset with valid attributes" do
8 | changeset = BuildManager.changeset(%Build{}, Map.put(params_for(:build), :project_id, 1))
9 | assert changeset.valid?
10 | end
11 |
12 | test "changeset with invalid attributes" do
13 | changeset = BuildManager.changeset(%Build{}, %{})
14 | refute changeset.valid?
15 | end
16 |
17 | test "changeset with real params" do
18 | params = Opencov.Fixtures.dummy_coverage
19 | changeset = BuildManager.changeset(build(:build, project: nil) |> with_project, params)
20 | assert changeset.valid?
21 |
22 | build = Repo.insert!(changeset)
23 | assert build.id
24 | assert build.commit_sha == params["git"]["head"]["id"]
25 | assert build.commit_message == params["git"]["head"]["message"]
26 | assert build.branch == params["git"]["branch"]
27 | assert build.service_name == params["service_name"]
28 | assert build.service_job_id == params["service_job_id"]
29 | end
30 |
31 | test "info_for when no service name and no previous build exists" do
32 | project = insert(:project)
33 | info = BuildManager.info_for(project, %{})
34 | assert info["build_number"] == 1
35 | end
36 |
37 | test "info_for when no service name and previous build exists" do
38 | previous_build = insert(:build) |> Repo.preload(:project)
39 | info = BuildManager.info_for(previous_build.project, %{})
40 | assert info["build_number"] == previous_build.build_number + 1
41 | end
42 |
43 | test "previous_build when no previous build" do
44 | build = insert(:build)
45 | assert build.previous_build_id == nil
46 | assert build.previous_coverage == nil
47 | end
48 |
49 | test "previous_build when previous build exists" do
50 | previous_build = insert(:build) |> Repo.preload(:project)
51 | build = insert(:build, project: previous_build.project, build_number: previous_build.build_number + 1)
52 | assert build.previous_build_id == previous_build.id
53 | assert build.previous_coverage == previous_build.coverage
54 | end
55 |
56 | test "get_or_create! when build does not exist" do
57 | project = insert(:project)
58 | cov = Opencov.Fixtures.dummy_coverage
59 | build = BuildManager.get_or_create!(project, cov)
60 | assert build.id
61 | assert build.commit_sha == cov["git"]["head"]["id"]
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 |
12 | ## From Ecto.Changeset.cast/4
13 | msgid "can't be blank"
14 | msgstr ""
15 |
16 | ## From Ecto.Changeset.unique_constraint/3
17 | msgid "has already been taken"
18 | msgstr ""
19 |
20 | ## From Ecto.Changeset.put_change/3
21 | msgid "is invalid"
22 | msgstr ""
23 |
24 | ## From Ecto.Changeset.validate_format/3
25 | msgid "has invalid format"
26 | msgstr ""
27 |
28 | ## From Ecto.Changeset.validate_subset/3
29 | msgid "has an invalid entry"
30 | msgstr ""
31 |
32 | ## From Ecto.Changeset.validate_exclusion/3
33 | msgid "is reserved"
34 | msgstr ""
35 |
36 | ## From Ecto.Changeset.validate_confirmation/3
37 | msgid "does not match confirmation"
38 | msgstr ""
39 |
40 | ## From Ecto.Changeset.no_assoc_constraint/3
41 | msgid "is still associated with this entry"
42 | msgstr ""
43 |
44 | msgid "are still associated with this entry"
45 | msgstr ""
46 |
47 | ## From Ecto.Changeset.validate_length/3
48 | msgid "should be %{count} character(s)"
49 | msgid_plural "should be %{count} character(s)"
50 | msgstr[0] ""
51 | msgstr[1] ""
52 |
53 | msgid "should have %{count} item(s)"
54 | msgid_plural "should have %{count} item(s)"
55 | msgstr[0] ""
56 | msgstr[1] ""
57 |
58 | msgid "should be at least %{count} character(s)"
59 | msgid_plural "should be at least %{count} character(s)"
60 | msgstr[0] ""
61 | msgstr[1] ""
62 |
63 | msgid "should have at least %{count} item(s)"
64 | msgid_plural "should have at least %{count} item(s)"
65 | msgstr[0] ""
66 | msgstr[1] ""
67 |
68 | msgid "should be at most %{count} character(s)"
69 | msgid_plural "should be at most %{count} character(s)"
70 | msgstr[0] ""
71 | msgstr[1] ""
72 |
73 | msgid "should have at most %{count} item(s)"
74 | msgid_plural "should have at most %{count} item(s)"
75 | msgstr[0] ""
76 | msgstr[1] ""
77 |
78 | ## From Ecto.Changeset.validate_number/3
79 | msgid "must be less than %{number}"
80 | msgstr ""
81 |
82 | msgid "must be greater than %{number}"
83 | msgstr ""
84 |
85 | msgid "must be less than or equal to %{number}"
86 | msgstr ""
87 |
88 | msgid "must be greater than or equal to %{number}"
89 | msgstr ""
90 |
91 | msgid "must be equal to %{number}"
92 | msgstr ""
93 |
--------------------------------------------------------------------------------
/web/templates/build/show.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= format_coverage(@build.coverage) %>
5 |
6 | <%= link @build.project.name, to: project_path(@conn, :show, @build.project) %>
7 | >
8 |
9 | #<%= @build.build_number %>
10 |
11 |
12 |
13 |
14 |
15 |
Latest change <%= human_time_ago(@build.inserted_at) %>
16 |
17 | <%= render "commit.html", build: @build %>
18 |
19 |
20 | <%= if @build.previous_coverage do %>
21 |
22 | <%= coverage_diff(@build.previous_coverage, @build.coverage) %>
23 |
24 | <% end %>
25 |
26 |
27 |
28 |
29 |
Jobs
30 |
31 |
32 |
33 |
34 | Number
35 | Coverage
36 | Diff
37 | Run time
38 | Files count
39 |
40 |
41 |
42 | <%= for job <- @build.jobs do %>
43 |
44 | <%= link "##{job.job_number}", to: job_path(@conn, :show, job) %>
45 |
46 | <%= format_coverage(job.coverage) %>
47 |
48 |
49 | <%= if job.previous_coverage do %>
50 | <%= render Opencov.SharedView, "coverage_diff.html", diff: job.coverage - job.previous_coverage %>
51 | <% end %>
52 |
53 | <%= job |> Opencov.JobView.job_time |> human_time_ago %>
54 | <%= job.files_count %>
55 |
56 | <% end %>
57 |
58 |
59 |
60 |
61 |
62 | <%= render Opencov.FileView,
63 | "list.html",
64 | conn: @conn,
65 | paginator: @paginator,
66 | files: @files,
67 | order: @order,
68 | filters: @filters,
69 | path_fn: &Opencov.Router.Helpers.build_path/4,
70 | path_args: [@conn, :show, @build]
71 | %>
72 |
--------------------------------------------------------------------------------
/web/controllers/admin/user_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Admin.UserController do
2 | use Opencov.Web, :controller
3 |
4 | import Opencov.Helpers.Authentication
5 |
6 | alias Opencov.UserService
7 | alias Opencov.User
8 | alias Opencov.UserManager
9 | alias Opencov.Repo
10 |
11 | plug :scrub_params, "user" when action in [:create, :update]
12 |
13 | def index(conn, params) do
14 | paginator = Repo.paginate(User, params)
15 | render(conn, "index.html", users: paginator.entries, paginator: paginator)
16 | end
17 |
18 | def new(conn, _params) do
19 | changeset = UserManager.changeset(%User{})
20 | render(conn, "new.html", changeset: changeset)
21 | end
22 |
23 | def create(conn, %{"user" => user_params}) do
24 | case UserService.create_user(user_params, invited?: true) do
25 | {:ok, user} ->
26 | conn
27 | |> put_flash(:info, "User created successfully.")
28 | |> redirect(to: admin_user_path(conn, :show, user))
29 | {:error, changeset} ->
30 | render(conn, "new.html", changeset: changeset)
31 | end
32 | end
33 |
34 | def show(conn, %{"id" => id}) do
35 | user = Repo.get!(User, id)
36 | render(conn, "show.html", user: user)
37 | end
38 |
39 | def edit(conn, %{"id" => id}) do
40 | user = Repo.get!(User, id)
41 | changeset = UserManager.changeset(user)
42 | render(conn, "edit.html", user: user, changeset: changeset)
43 | end
44 |
45 | def update(conn, %{"id" => id, "user" => user_params}) do
46 | user = Repo.get!(User, id)
47 | changeset = UserManager.changeset(user, user_params)
48 |
49 | case Repo.update(changeset) do
50 | {:ok, user} ->
51 | redirect_path = NavigationHistory.last_path(conn, 1, default: admin_user_path(conn, :show, user))
52 | conn
53 | |> put_flash(:info, "user updated successfully.")
54 | |> redirect(to: redirect_path)
55 | {:error, changeset} ->
56 | render(conn, "edit.html", user: user, changeset: changeset)
57 | end
58 | end
59 |
60 | def delete(conn, %{"id" => id}) do
61 | user = Repo.get!(User, id)
62 | if current_user(conn).id == user.id do
63 | conn
64 | |> put_flash(:error, "You cannot delete yourself.")
65 | |> redirect(to: NavigationHistory.last_path(conn, default: admin_user_path(conn, :index)))
66 | else
67 | Repo.delete!(user)
68 |
69 | conn
70 | |> put_flash(:info, "User deleted successfully.")
71 | |> redirect(to: admin_user_path(conn, :index))
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/web/templates/file/list.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
Files (<%= @paginator.total_entries %>)
3 |
4 | <%= for {k, v} <- filters() do %>
5 | <% order_args = [order_field: elem(@order, 0), order_direction: elem(@order, 1)] %>
6 | <%= if Enum.any?(@filters, &(&1 == k)) do %>
7 |
8 | <%= link v, to: apply(@path_fn, @path_args ++ [[{:filters, @filters -- [k]}|order_args]]) %>
9 |
10 | <% else %>
11 |
12 | <%= link v, to: apply(@path_fn, @path_args ++ [[{:filters, [k|@filters]}|order_args]]) %>
13 |
14 | <% end %>
15 | <% end %>
16 |
17 |
18 |
19 |
20 |
21 | <%= for {k, v} <- %{"coverage" => "Coverage", "diff" => "Diff", "name" => "Name"} do %>
22 | <% order = if elem(@order, 0) == k && elem(@order, 1) == "desc", do: "asc", else: "desc" %>
23 |
24 | <%= link to: apply(@path_fn, @path_args ++ [[filters: @filters, order_field: k, order_direction: order]]) do %>
25 | <%= v %>
26 | <%= if elem(@order, 0) == k do %>
27 |
28 | <% end %>
29 | <% end %>
30 |
31 | <% end %>
32 |
33 |
34 |
35 | <%= for file <- @files do %>
36 |
37 |
38 | <%= format_coverage(file.coverage) %>
39 |
40 |
41 | <%= if file.previous_coverage do %>
42 | <%= render Opencov.SharedView, "coverage_diff.html", diff: file.coverage - file.previous_coverage %>
43 | <% end %>
44 |
45 | <%= link file.name, to: file_path(@conn, :show, file) %>
46 |
47 | <% end %>
48 |
49 |
50 |
51 | <%= if @paginator.total_pages > 1 do %>
52 |
53 | <%= pagination_links @conn,
54 | @paginator,
55 | Enum.drop(@path_args, 2),
56 | path: @path_fn,
57 | action: Enum.at(@path_args, 1),
58 | filters: @filters,
59 | order_field: elem(@order, 0),
60 | order_direction: elem(@order, 1)
61 | %>
62 |
63 | <% end %>
64 |
65 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
3 | var path = require('path')
4 | var nib = require('nib')
5 |
6 | const phoenixHTMLPath = './deps/phoenix_html/priv/static/phoenix_html.js'
7 |
8 | module.exports = {
9 | entry: {
10 | app: './web/static/js/app.js',
11 | theme: './web/static/css/theme.less',
12 | vendor: [
13 | 'jquery',
14 | 'lodash',
15 | 'riot',
16 | 'highlight.js',
17 | 'bootstrap',
18 | 'font-awesome/css/font-awesome.css',
19 | 'highlight.js/styles/solarized-light.css'
20 | ]
21 | },
22 | output: {
23 | path: path.join(__dirname, './priv/static/js'),
24 | filename: '[name].js'
25 | },
26 | devtool: 'source-map',
27 | module: {
28 | rules: [
29 | {test: /\.json$/, loader: 'json-loader'},
30 | {
31 | test: /\.js$/,
32 | loader: 'babel-loader',
33 | options: {
34 | presets: ['es2015'],
35 | plugins: ['transform-runtime']
36 | },
37 | include: /web\/static\/js/
38 | },
39 | {test: /\.jade$/, loader: 'pug-loader'},
40 | {
41 | test: /\.styl$/,
42 | loader: ExtractTextPlugin.extract({
43 | fallback: 'style-loader',
44 | use: [
45 | 'css-loader',
46 | {loader: 'stylus-loader', options: {use: [nib()]}}
47 | ]
48 | })
49 | },
50 | {
51 | test: /\.less$/,
52 | loader: ExtractTextPlugin.extract({
53 | fallback: 'style-loader',
54 | use: ['css-loader', 'less-loader']
55 | })
56 | },
57 | {
58 | test: /\.css$/,
59 | loader: ExtractTextPlugin.extract({
60 | fallback: 'style-loader',
61 | use: ['css-loader']
62 | })
63 | },
64 | {
65 | test: /\.(png|woff|woff2|eot|ttf|svg|gif)/,
66 | loader: 'url-loader?limit=10000'
67 | },
68 | {
69 | test: /\.jpg/,
70 | loader: 'file-loader'
71 | }
72 | ]
73 | },
74 | resolve: {
75 | alias: {
76 | phoenix_html: path.join(__dirname, phoenixHTMLPath)
77 | }
78 | },
79 | plugins: [
80 | new ExtractTextPlugin('[name].css', {allChunks: true}),
81 | new webpack.optimize.CommonsChunkPlugin({
82 | name: 'vendor',
83 | minChunks: Infinity
84 | }),
85 | new webpack.ProvidePlugin({
86 | $: 'jquery',
87 | jQuery: 'jquery',
88 | 'window.jQuery': 'jquery'
89 | })
90 | ]
91 | }
92 |
--------------------------------------------------------------------------------
/lib/opencov/badge_creator.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BadgeCreator do
2 | require EEx
3 |
4 | # values and SVG are taken from https://github.com/badges/shields
5 | @base_width 89
6 | @extra_width 7
7 | @template_path Path.join(__DIR__, "templates/badge_template.eex")
8 | @authorized_formats ~w(png jpg svg)
9 |
10 | EEx.function_from_file :defp, :template, @template_path, [:coverage_str, :width, :extra_width, :bg_color]
11 |
12 | def make_badge(coverage, options \\ []) do
13 | {coverage, coverage_str, digits_num} = if is_nil(coverage) do
14 | {nil, "NA", 2}
15 | else
16 | coverage = round(coverage)
17 | {coverage, "#{coverage}%", coverage |> Integer.to_string |> String.length}
18 | end
19 | extra_width = (digits_num - 1) * @extra_width
20 | width = @base_width + extra_width
21 | color = badge_color(coverage)
22 | template(coverage_str, width, extra_width, color) |> get_image(options[:format])
23 | end
24 |
25 | defp get_image(svg, nil),
26 | do: get_image(svg, "png")
27 | defp get_image(svg, format) when format in @authorized_formats,
28 | do: get_image(svg, String.to_atom(format))
29 | defp get_image(svg, :svg),
30 | do: {:ok, :svg, svg}
31 | defp get_image(svg, format) when is_atom(format),
32 | do: transform(svg, format)
33 |
34 | def transform(svg, format) do
35 | dir = Temp.mkdir!("opencov")
36 | {svg_path, output_path} = {Path.join(dir, "coverage.svg"), Path.join(dir, "coverage.#{format}")}
37 | File.write!(svg_path, svg)
38 | case make_output(svg_path, output_path) do
39 | {:ok, output} ->
40 | File.rm_rf!(dir)
41 | {:ok, format, output}
42 | e -> e
43 | end
44 | end
45 |
46 | defp make_output(svg_path, output_path) do
47 | try do
48 | Opencov.ImageMagick.convert([svg_path, output_path])
49 | File.read(output_path)
50 | rescue
51 | ErlangError -> {:error, "failed to run convert"}
52 | end
53 | end
54 |
55 | defp badge_color(coverage) do
56 | color = cond do
57 | is_nil(coverage) -> "lightgrey"
58 | coverage == 0 -> "red"
59 | coverage < 80 -> "yellow"
60 | coverage < 90 -> "yellowgreen"
61 | coverage < 100 -> "green"
62 | true -> "brightgreen"
63 | end
64 | hex_color(color)
65 | end
66 |
67 | defp hex_color("red"), do: "#e05d44"
68 | defp hex_color("yellow"), do: "#dfb317"
69 | defp hex_color("yellowgreen"), do: "#a4a61d"
70 | defp hex_color("green"), do: "#97CA00"
71 | defp hex_color("brightgreen"), do: "#4c1"
72 | defp hex_color("lightgrey"), do: "#9f9f9f"
73 | end
74 |
--------------------------------------------------------------------------------
/web/managers/job_manager.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.JobManager do
2 | use Opencov.Web, :manager
3 |
4 | import Ecto.Query
5 | import Opencov.Job
6 | alias Opencov.Job
7 | alias Opencov.FileManager
8 |
9 | @required_fields ~w(build_id)a
10 | @optional_fields ~w(run_at job_number files_count)a
11 |
12 | def changeset(model, params \\ :invalid) do
13 | model
14 | |> cast(params, @required_fields ++ @optional_fields)
15 | |> validate_required(@required_fields)
16 | |> prepare_changes(&check_job_number/1)
17 | |> prepare_changes(&set_previous_values/1)
18 | end
19 |
20 | defp check_job_number(changeset) do
21 | if get_change(changeset, :job_number) do
22 | changeset
23 | else
24 | set_job_number(changeset)
25 | end
26 | end
27 |
28 | defp set_job_number(changeset) do
29 | build_id = get_change(changeset, :build_id) || changeset.data.build_id
30 | job = Job |> for_build(build_id) |> order_by(desc: :job_number) |> Repo.first
31 | job_number = if job, do: job.job_number + 1, else: 1
32 | put_change(changeset, :job_number, job_number)
33 | end
34 |
35 | defp set_previous_values(changeset) do
36 | build_id = get_change(changeset, :build_id) || changeset.data.build_id
37 | job_number = get_change(changeset, :job_number)
38 | previous_build_id = Opencov.Repo.get!(Opencov.Build, build_id).previous_build_id
39 | previous_job = search_previous_job(previous_build_id, job_number)
40 | if previous_job do
41 | change(changeset, %{previous_job_id: previous_job.id, previous_coverage: previous_job.coverage})
42 | else
43 | changeset
44 | end
45 | end
46 |
47 | defp search_previous_job(nil, _), do: nil
48 | defp search_previous_job(previous_build_id, job_number),
49 | do: Job |> for_build(previous_build_id) |> where(job_number: ^job_number) |> Repo.first
50 |
51 | def update_coverage(job) do
52 | job = change(job, coverage: compute_coverage(job)) |> Repo.update! |> Repo.preload(:build)
53 | Opencov.BuildManager.update_coverage(job.build)
54 | job
55 | end
56 |
57 | def create_from_json!(build, params) do
58 | {source_files, params} = Map.pop(params, "source_files", [])
59 | params = Map.put(params, "files_count", Enum.count(source_files))
60 | params = Map.update(params, "run_at", nil, fn time -> String.replace(time, " +", "+") end)
61 | job = Ecto.build_assoc(build, :jobs) |> changeset(params) |> Repo.insert!
62 | Enum.each source_files, fn file_params ->
63 | Ecto.build_assoc(job, :files) |> FileManager.changeset(file_params) |> Repo.insert!
64 | end
65 | job |> Repo.preload(:files) |> update_coverage
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/web/controllers/project_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ProjectController do
2 | use Opencov.Web, :controller
3 |
4 | import Opencov.Helpers.Authentication
5 |
6 | alias Opencov.Project
7 | alias Opencov.ProjectManager
8 |
9 | plug :scrub_params, "project" when action in [:create, :update]
10 |
11 | def index(conn, _params) do
12 | projects = Repo.all(Project) |> ProjectManager.preload_latest_build
13 | render(conn, "index.html", projects: projects)
14 | end
15 |
16 | def new(conn, _params) do
17 | changeset = ProjectManager.changeset(%Project{})
18 | render(conn, "new.html", changeset: changeset)
19 | end
20 |
21 | def create(conn, %{"project" => project_params}) do
22 | project = Ecto.build_assoc(current_user(conn), :projects)
23 | changeset = ProjectManager.changeset(project, project_params)
24 |
25 | case Repo.insert(changeset) do
26 | {:ok, project} ->
27 | conn
28 | |> put_flash(:info, "Project created successfully.")
29 | |> redirect(to: project_path(conn, :show, project))
30 | {:error, changeset} ->
31 | render(conn, "new.html", changeset: changeset)
32 | end
33 | end
34 |
35 | def show(conn, %{"id" => id}) do
36 | project = Repo.get!(Project, id) |> ProjectManager.preload_recent_builds
37 | render(conn, "show.html", project: project)
38 | end
39 |
40 | def edit(conn, %{"id" => id}) do
41 | project = Repo.get!(Project, id)
42 | changeset = ProjectManager.edit_changeset(project)
43 | render(conn, "edit.html", project: project, changeset: changeset)
44 | end
45 |
46 | def update(conn, %{"id" => id, "project" => project_params}) do
47 | project = Repo.get!(Project, id)
48 | changeset = ProjectManager.edit_changeset(project, project_params)
49 |
50 | case Repo.update(changeset) do
51 | {:ok, project} ->
52 | conn
53 | |> put_flash(:error, "Project updated successfully.")
54 | |> redirect(to: project_path(conn, :show, project))
55 | {:error, changeset} ->
56 | render(conn, "edit.html", project: project, changeset: changeset)
57 | end
58 | end
59 |
60 | def delete(conn, %{"id" => id}) do
61 | project = Repo.get!(Project, id)
62 | Repo.delete!(project)
63 |
64 | conn
65 | |> put_flash(:info, "Project deleted successfully.")
66 | |> redirect(to: project_path(conn, :index))
67 | end
68 |
69 | def badge(conn, %{"project_id" => id, "format" => format}) do
70 | project = Repo.get!(Project, id)
71 | {:ok, badge} = Opencov.BadgeManager.get_or_create(project, format)
72 | conn
73 | |> put_resp_content_type(MIME.type(format))
74 | |> send_resp(200, badge.image)
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/web/templates/project/show.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= format_coverage(@project.current_coverage) %>
5 |
6 | <%= @project.name %>
7 |
8 |
9 |
10 | <%= if @project.base_url &&
11 | (String.starts_with?(@project.base_url, "http://") ||
12 | String.starts_with?(@project.base_url, "https://")) do %>
13 |
14 | <%= link to: @project.base_url, class: "inline-block" do %>
15 |
16 | View repository
17 | <% end %>
18 |
19 | <% end %>
20 |
21 |
24 |
25 |
36 |
37 |
38 |
Recent builds
39 |
40 |
41 |
42 |
43 | Build
44 | Branch
45 | Coverage
46 | Diff
47 | Commit
48 | Committer
49 | Time
50 | Via
51 |
52 |
53 |
54 | <%= for build <- @project.builds do %>
55 |
56 | #<%= build.build_number %>
57 | <%= build.branch %>
58 |
59 | <%= format_coverage(build.coverage) %>
60 |
61 |
62 | <%= if build.previous_coverage do %>
63 | <%= render Opencov.SharedView, "coverage_diff.html", diff: build.coverage - build.previous_coverage %>
64 | <% end %>
65 |
66 | <%= render Opencov.BuildView, "commit.html", build: %{build|project: @project} %>
67 | <%= build.committer_name %>
68 | <%= human_time_ago(build.inserted_at) %>
69 | <%= build.service_name %>
70 |
71 | <% end %>
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/web/managers/build_manager.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.BuildManager do
2 | use Opencov.Web, :manager
3 |
4 | alias Opencov.Build
5 | import Opencov.Build
6 |
7 | @required_fields ~w(build_number)a
8 | @optional_fields ~w(commit_sha commit_message committer_name committer_email branch
9 | service_name service_job_id service_job_pull_request project_id completed)a
10 |
11 | def changeset(model, params \\ :invalid) do
12 | model
13 | |> cast(normalize_params(params), @required_fields ++ @optional_fields)
14 | |> validate_required(@required_fields)
15 | |> set_build_started_at
16 | |> prepare_changes(&add_previous_values/1)
17 | end
18 |
19 | def create_from_json!(project, params) do
20 | params = Map.merge(params, info_for(project, params))
21 | build = Ecto.build_assoc(project, :builds)
22 | Repo.insert! changeset(build, params)
23 | end
24 |
25 | def get_or_create!(project, params) do
26 | current_build = current_for_project(Build, project) |> Repo.first
27 | git_params = Map.get(params, "git", %{})
28 | if build = (current_build || Repo.first(for_commit(project, git_params))),
29 | do: build,
30 | else: create_from_json!(project, params)
31 | end
32 |
33 | def update_coverage(build) do
34 | coverage = build |> Repo.preload(:jobs) |> compute_coverage
35 | build = Repo.update! change(build, coverage: coverage)
36 | Opencov.ProjectManager.update_coverage(Repo.preload(build, :project).project)
37 | build
38 | end
39 |
40 | defp set_build_started_at(changeset) do
41 | if get_change(changeset, :build_started_at), do: changeset,
42 | else: put_change(changeset, :build_started_at, DateTime.utc_now())
43 | end
44 |
45 | # TODO: fetch build/job numbers from CI APIs
46 | # def info_for(_project, %{"service_name" => "travis-ci"}), do: %{"build_number" => 1, "job_number" => 1}
47 | def info_for(project, params), do: fallback_info_for(project, params)
48 |
49 | defp fallback_info_for(project, _params) do
50 | build = query_for_project(project.id) |> order_by_build_number |> Repo.first
51 | if build, do: %{"build_number" => build.build_number + 1}, else: %{"build_number" => 1}
52 | end
53 |
54 | defp add_previous_values(changeset) do
55 | project_id = changeset.data.project_id || get_change(changeset, :project_id)
56 | if previous_build = search_previous_build(changeset, project_id) do
57 | change(changeset, %{previous_build_id: previous_build.id, previous_coverage: previous_build.coverage})
58 | else
59 | changeset
60 | end
61 | end
62 |
63 | defp search_previous_build(changeset, project_id) do
64 | Build.previous(project_id,
65 | get_change(changeset, :build_number),
66 | get_change(changeset, :branch))
67 | |> Opencov.Repo.first
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/web/controllers/profile_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ProfileController do
2 | use Opencov.Web, :controller
3 |
4 | import Opencov.Helpers.Authentication
5 |
6 | alias Opencov.User
7 | alias Opencov.UserManager
8 |
9 | def show(conn, _params) do
10 | user = current_user(conn)
11 | render(conn, "edit.html", user: user, changeset: UserManager.changeset(user))
12 | end
13 |
14 | def update(conn, params) do
15 | case Opencov.UserService.update_user(params, current_user(conn)) do
16 | {:ok, _user, redirect_path, flash_message} ->
17 | conn
18 | |> put_flash(:info, flash_message)
19 | |> redirect(to: redirect_path)
20 | {:error, assigns} ->
21 | render(conn, "edit.html", assigns)
22 | end
23 | end
24 |
25 | def edit_password(conn, _params) do
26 | user = current_user(conn)
27 | render(conn, "edit_password.html", user: user, changeset: UserManager.changeset(user))
28 | end
29 |
30 | def update_password(conn, %{"user" => user_params}) do
31 | user = current_user(conn)
32 | changeset = UserManager.password_update_changeset(user, user_params)
33 | case Repo.update(changeset) do
34 | {:ok, _user} -> conn |> put_flash(:info, "Your password has been updated") |> redirect(to: "/")
35 | {:error, changeset} -> render(conn, "edit_password.html", user: user, changeset: changeset)
36 | end
37 | end
38 |
39 | def reset_password_request(conn, _params) do
40 | render(conn, "reset_password_request.html")
41 | end
42 |
43 | def send_reset_password(conn, %{"user" => %{"email" => email}}) do
44 | Opencov.UserService.send_reset_password(email)
45 | conn
46 | |> put_flash(:info, "An email has been sent to reset your password.")
47 | |> redirect(to: auth_path(conn, :login))
48 | end
49 |
50 | def reset_password(conn, %{"token" => token}) do
51 | case Repo.get_by(User, password_reset_token: token) do
52 | %User{} = user ->
53 | changeset = UserManager.changeset(user)
54 | render(conn, "reset_password.html", user: user, changeset: changeset, token: token)
55 | _ -> password_reset_error(conn)
56 | end
57 | end
58 |
59 | def finalize_reset_password(conn, %{"user" => %{"password_reset_token" => token} = user_params}) do
60 | case Opencov.UserService.finalize_reset_password(user_params) do
61 | {:ok, user} ->
62 | conn
63 | |> put_flash(:info, "Your password has been reset.")
64 | |> Opencov.Authentication.login(user)
65 | |> redirect(to: "/")
66 | {:error, :not_found} -> password_reset_error(conn)
67 | {:error, changeset} ->
68 | render(conn, "reset_password.html", user: changeset.data, changeset: changeset, token: token)
69 | end
70 | end
71 |
72 | defp password_reset_error(conn) do
73 | conn
74 | |> put_flash(:error, "Could not reset your password. Check your email or try the process again.")
75 | |> redirect(to: auth_path(conn, :login))
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/web/services/user_service.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.UserService do
2 | alias Opencov.User
3 | alias Opencov.UserManager
4 | alias Opencov.UserMailer
5 | alias Opencov.Repo
6 |
7 | def create_user(user_params, opts) do
8 | options = [generate_password: opts[:invited?]]
9 | changeset = UserManager.changeset(%User{}, user_params, options)
10 | case Repo.insert(changeset) do
11 | {:ok, user} = res ->
12 | email = UserMailer.confirmation_email(user, opts ++ [registration: true])
13 | Opencov.AppMailer.send(email)
14 | res
15 | err -> err
16 | end
17 | end
18 |
19 | def confirm_user(token) do
20 | case Repo.get_by(User, confirmation_token: token) do
21 | %User{unconfirmed_email: m} = user when not is_nil(m) ->
22 | finalize_confirmation!(user)
23 | {:ok, user, "Your email has been confirmed successfully"}
24 | %User{} = user -> {:ok, user, "You are already confirmed."}
25 | _ -> {:error, "Could not find the user to confirm"}
26 | end
27 | end
28 |
29 | defp finalize_confirmation!(user) do
30 | UserManager.confirmation_changeset(user) |> Repo.update!
31 | end
32 |
33 | def send_reset_password(email) do
34 | case Repo.get_by(User, email: email) do
35 | %User{} = user ->
36 | UserManager.password_reset_changeset(user)
37 | |> Repo.update!
38 | |> UserMailer.reset_password_email
39 | |> Opencov.AppMailer.send
40 | :ok
41 | _ -> :ok
42 | end
43 | end
44 |
45 | def finalize_reset_password(%{"password_reset_token" => token} = params) do
46 | case Repo.get_by(User, password_reset_token: token) do
47 | %User{} = user ->
48 | opts = [skip_password_validation: true, remove_reset_token: true]
49 | UserManager.password_update_changeset(user, params, opts) |> Repo.update
50 | _ -> {:error, :not_found}
51 | end
52 | end
53 |
54 | def update_user(%{"user" => user_params}, user) do
55 | redirect_path = Opencov.Router.Helpers.profile_path(Opencov.Endpoint, :show)
56 | changeset = Opencov.UserManager.changeset(user, user_params)
57 |
58 | case Opencov.Repo.update(changeset) do
59 | {:ok, user} ->
60 | send_confirmation_email_if_needed(user, changeset)
61 | {:ok, user, redirect_path, update_flash_message(changeset)}
62 | {:error, changeset} ->
63 | {:error, user: user, changeset: changeset}
64 | end
65 | end
66 |
67 | defp send_confirmation_email_if_needed(user, changeset) do
68 | if email_changed?(changeset), do: send_confirmation_email(user)
69 | end
70 |
71 | defp send_confirmation_email(user) do
72 | email = Opencov.UserMailer.confirmation_email(user)
73 | Opencov.AppMailer.send(email)
74 | end
75 |
76 | defp update_flash_message(changeset) do
77 | "Your profile has been changed successfully." <>
78 | if email_changed?(changeset), do: " Please confirm your email.", else: ""
79 | end
80 |
81 | defp email_changed?(changeset) do
82 | not is_nil(Ecto.Changeset.get_change(changeset, :unconfirmed_email))
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Router do
2 | use Opencov.Web, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | plug Opencov.Plug.FetchUser
11 | plug Opencov.Plug.ForcePasswordInitialize
12 | plug NavigationHistory.Tracker, excluded_paths: ~w(/login /users/new)
13 | if Application.get_env(:opencov, :auth)[:enable] do
14 | plug BasicAuth, use_config: {:opencov, :auth}
15 | end
16 | end
17 |
18 | pipeline :anonymous_only do
19 | plug Opencov.Plug.AnonymousOnly
20 | end
21 |
22 | pipeline :authenticate do
23 | plug Opencov.Plug.Authentication
24 | end
25 |
26 | pipeline :authenticate_admin do
27 | plug Opencov.Plug.Authentication, admin: true
28 | end
29 |
30 | pipeline :api do
31 | plug :accepts, ["json"]
32 | end
33 |
34 | scope "/api/v1", Opencov.Api.V1, as: :api_v1 do
35 | pipe_through :api
36 |
37 | resources "/jobs", JobController, only: [:create]
38 | end
39 |
40 | scope "/", Opencov do
41 | pipe_through :browser
42 |
43 | get "/projects/:project_id/badge.:format", ProjectController, :badge, as: :project_badge
44 | end
45 |
46 | scope "/", Opencov do
47 | pipe_through :browser
48 | pipe_through :anonymous_only
49 |
50 | get "/login", AuthController, :login
51 | post "/login", AuthController, :make_login
52 | resources "/users", UserController, only: [:new, :create]
53 | get "/users/confirm", UserController, :confirm
54 | get "/profile/password/reset_request", ProfileController, :reset_password_request
55 | post "/profile/password/reset_request", ProfileController, :send_reset_password
56 | get "/profile/password/reset", ProfileController, :reset_password
57 | put "/profile/password/reset", ProfileController, :finalize_reset_password
58 | end
59 |
60 | scope "/", Opencov do
61 | pipe_through :browser
62 | pipe_through :authenticate
63 |
64 | delete "/logout", AuthController, :logout
65 |
66 | get "/", ProjectController, :index
67 |
68 | get "/profile", ProfileController, :show
69 | put "/profile", ProfileController, :update
70 |
71 | if not Opencov.Helpers.Authentication.demo?() do
72 | get "/profile/password/edit", ProfileController, :edit_password
73 | put "/profile/password", ProfileController, :update_password
74 | end
75 |
76 | resources "/projects", ProjectController
77 | resources "/builds", BuildController, only: [:show]
78 | resources "/files", FileController, only: [:show]
79 |
80 | resources "/jobs", JobController, only: [:show]
81 | end
82 |
83 | scope "/admin", Opencov.Admin, as: :admin do
84 | pipe_through :browser
85 | pipe_through :authenticate_admin
86 |
87 | get "/", DashboardController, :index
88 |
89 | resources "/users", UserController
90 | resources "/projects", ProjectController, only: [:index, :show]
91 | get "/settings", SettingsController, :edit
92 | put "/settings", SettingsController, :update
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/web/models/file.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.File do
2 | use Opencov.Web, :model
3 |
4 | import Ecto.Query
5 |
6 | defimpl Jason.Encoder, for: Opencov.File do
7 | def encode(model, opts) do
8 | model
9 | |> Map.take([:name, :source])
10 | |> Map.put(:coverage, model.coverage_lines)
11 | |> Jason.Encoder.encode(opts)
12 | end
13 | end
14 |
15 | alias Opencov.Job
16 |
17 | schema "files" do
18 | field :name, :string
19 | field :source, :string
20 | field :coverage, :float
21 | field :previous_coverage, :float
22 | field :coverage_lines, Opencov.Types.JSON
23 |
24 | belongs_to :job, Job
25 | belongs_to :previous_file, Opencov.File
26 |
27 | timestamps()
28 | end
29 |
30 | @allowed_sort_fields ~w(name coverage diff)
31 |
32 | def sort_by(query, param, order) when order in ~w(asc desc),
33 | do: sort_by(query, param, String.to_atom(order))
34 | def sort_by(query, param, order) when param in @allowed_sort_fields,
35 | do: sort_by(query, String.to_atom(param), order)
36 | def sort_by(query, :diff, order) do
37 | query |> order_by([f], [{^order, fragment("abs(? - ?)", f.previous_coverage, f.coverage)}])
38 | end
39 | def sort_by(query, param, order) do
40 | order = if __schema__(:type, param) == :string,
41 | do: order,
42 | else: reverse_order(order)
43 | query |> order_by([f], [{^order, field(f, ^param)}])
44 | end
45 |
46 | defp reverse_order(:asc), do: :desc
47 | defp reverse_order(:desc), do: :asc
48 |
49 | def for_job(query \\ Opencov.File, job)
50 |
51 | def for_job(query, jobs) when is_list(jobs), do: query |> where([f], f.job_id in ^jobs)
52 | def for_job(query, %Opencov.Job{id: job_id}), do: for_job(query, job_id)
53 | def for_job(query, job_id), do: query |> where([f], f.job_id == ^job_id)
54 |
55 | def with_filters(query, [filter|rest]), do: with_filters(with_filter(query, filter), rest)
56 | def with_filters(query, []), do: query
57 |
58 | def with_filter(query, "cov_changed") do
59 | query |> where([f], f.coverage != f.previous_coverage)
60 | end
61 | def with_filter(query, "changed") do
62 | query
63 | |> join(:left, [f], of in assoc(f, :previous_file))
64 | |> where([f, of], f.source != of.source or is_nil(of.source))
65 | end
66 | def with_filter(query, "covered"), do: query |> where([f], f.coverage > 0.0)
67 | def with_filter(query, "unperfect"), do: query |> where([f], f.coverage < 100.0)
68 | def with_filter(query, _), do: query
69 |
70 | def with_name(query, name) do
71 | query |> where(name: ^name)
72 | end
73 |
74 | def order_by_coverage(query, order \\ :desc) do
75 | query |> order_by([f], [{^order, f.coverage}])
76 | end
77 |
78 | def compute_coverage(lines) do
79 | relevant_count = relevant_lines_count(lines)
80 | if relevant_count == 0,
81 | do: 0.0,
82 | else: covered_lines_count(lines) * 100 / relevant_count
83 | end
84 |
85 | def relevant_lines_count(lines),
86 | do: lines |> Enum.reject(fn n -> is_nil(n) end) |> Enum.count
87 |
88 | def covered_lines_count(lines),
89 | do: lines |> Enum.reject(fn n -> is_nil(n) or n == 0 end) |> Enum.count
90 | end
91 |
--------------------------------------------------------------------------------
/web/managers/user_manager.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.UserManager do
2 | use Opencov.Web, :manager
3 |
4 | import SecurePassword, only: [with_secure_password: 1]
5 |
6 | @required_fields ~w(email)a
7 | @optional_fields ~w(admin name password)a
8 |
9 | def changeset(model, params \\ :invalid, opts \\ []) do
10 | model
11 | |> cast(params, @required_fields ++ @optional_fields)
12 | |> validate_required(@required_fields)
13 | |> unique_constraint(:email)
14 | |> validate_email
15 | |> assign_unconfirmed_email
16 | |> unique_constraint(:unconfirmed_email)
17 | |> pipe_when(opts[:generate_password], generate_password)
18 | |> with_secure_password
19 | end
20 |
21 | def confirmation_changeset(model) do
22 | Ecto.Changeset.change(model)
23 | |> put_change(:email, model.unconfirmed_email)
24 | |> put_change(:unconfirmed_email, nil)
25 | |> pipe_when(is_nil(model.confirmed_at), put_change(:confirmed_at, Timex.now))
26 | end
27 |
28 | def password_update_changeset(model, params \\ :invalid, opts \\ []) do
29 | model
30 | |> cast(params, ~w(password password_confirmation current_password)a)
31 | |> validate_required(~w(password password_confirmation)a)
32 | |> pipe_when(!opts[:skip_password_validation], validate_password_update)
33 | |> pipe_when(opts[:remove_reset_token], remove_reset_token)
34 | |> put_change(:password_initialized, true)
35 | |> with_secure_password
36 | end
37 |
38 | def password_reset_changeset(model) do
39 | change(model)
40 | |> generate_password_reset_token
41 | |> put_change(:password_reset_sent_at, Timex.now)
42 | end
43 |
44 | defp remove_reset_token(changeset) do
45 | change(changeset, password_reset_token: nil, password_reset_sent_at: nil)
46 | end
47 |
48 | defp validate_password_update(changeset) do
49 | user = changeset.data
50 | if !user.password_initialized or Opencov.User.authenticate(user, get_change(changeset, :current_password)) do
51 | delete_change(changeset, :current_password)
52 | else
53 | add_error(changeset, :current_password, "is invalid")
54 | end
55 | end
56 |
57 | defp generate_password(changeset) do
58 | change(changeset, password: SecureRandom.urlsafe_base64(12), password_initialized: false)
59 | end
60 |
61 | defp generate_password_reset_token(changeset) do
62 | put_change(changeset, :password_reset_token, SecureRandom.urlsafe_base64(30))
63 | end
64 |
65 | defp validate_email(%Ecto.Changeset{} = changeset) do
66 | email = get_change(changeset, :email)
67 | error = email && validate_email_format(email)
68 | if email && error do
69 | add_error(changeset, :email, error)
70 | else
71 | changeset
72 | end
73 | end
74 |
75 | defp validate_email_format(email) do
76 | if not Regex.match?(~r/@/, email) do
77 | "the email is not valid"
78 | else
79 | validate_domain(email)
80 | end
81 | end
82 |
83 | defp assign_unconfirmed_email(changeset) do
84 | if new_email = get_change(changeset, :email) do
85 | changeset
86 | |> put_change(:unconfirmed_email, new_email)
87 | |> put_change(:confirmation_token, SecureRandom.urlsafe_base64(30))
88 | |> delete_change(:email)
89 | else
90 | changeset
91 | end
92 | end
93 |
94 | defp validate_domain(email) do
95 | allowed_domains = Opencov.SettingsManager.restricted_signup_domains
96 | domain = email |> String.split("@") |> List.last
97 | if allowed_domains && not domain in allowed_domains do
98 | "only the following domains are allowed: #{Enum.join(allowed_domains, ",")}"
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/web/models/build.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Build do
2 | use Opencov.Web, :model
3 |
4 | @git_defaults %{"branch" => nil, "head" => %{"id" => nil, "committer_name" => nil,
5 | "committer_email" => nil, "message" => nil }}
6 |
7 | import Ecto.Query
8 |
9 | schema "builds" do
10 | field :build_number, :integer
11 | field :previous_build_id, :integer
12 | field :coverage, :float, default: 0.0
13 | field :completed, :boolean, default: true
14 | field :previous_coverage, :float
15 | field :build_started_at, :utc_datetime
16 |
17 | field :commit_sha, :string
18 | field :committer_name, :string
19 | field :committer_email, :string
20 | field :commit_message, :string
21 | field :branch, :string, default: ""
22 |
23 | field :service_name, :string
24 | field :service_job_id, :string
25 | field :service_job_pull_request, :string
26 |
27 | belongs_to :project, Opencov.Project
28 | has_many :jobs, Opencov.Job
29 | has_one :previous_build, Opencov.Build, foreign_key: :previous_build_id
30 |
31 | timestamps()
32 | end
33 |
34 |
35 | def previous(project_id, build_number, nil),
36 | do: previous(project_id, build_number, "")
37 | def previous(project_id, build_number, branch) do
38 | query_for_project(project_id)
39 | |> where([b], b.build_number < ^build_number and b.branch == ^branch)
40 | |> order_by_build_number
41 | |> first
42 | end
43 |
44 | def current_for_project(query, project) do
45 | query |> for_project(project.id) |> where([b], not b.completed)
46 | end
47 |
48 | def last_for_project(query, project) do
49 | query |> for_project(project.id) |> order_by_build_number |> first
50 | end
51 |
52 | def query_for_project(project_id) do
53 | for_project(Opencov.Build, project_id)
54 | end
55 |
56 | def for_project(query, project_id) do
57 | query |> where([b], b.project_id == ^project_id)
58 | end
59 |
60 | def order_by_build_number(query) do
61 | query |> order_by([b], [desc: b.build_number])
62 | end
63 |
64 | def normalize_params(params) when is_map(params) do
65 | {git_info, params} = Map.pop(params, "git")
66 | Map.merge(params, git_params(git_info))
67 | end
68 | def normalize_params(params), do: params
69 |
70 | defp git_params(nil), do: %{}
71 | defp git_params(params) do
72 | params = Map.merge(@git_defaults, params)
73 | params = Map.put(params, "head", Map.merge(@git_defaults["head"], params["head"]))
74 | %{
75 | "branch" => branch,
76 | "head" => %{
77 | "id" => commit_sha,
78 | "committer_name" => committer_name,
79 | "committer_email" => committer_email,
80 | "message" => commit_message
81 | }
82 | } = params
83 | result = %{"branch" => branch || "", "commit_sha" => commit_sha, "committer_name" => committer_name,
84 | "committer_email" => committer_email, "commit_message" => commit_message}
85 | for {k, v} <- result, into: %{} do
86 | v = if is_nil(v), do: v, else: String.trim(v)
87 | {k, v}
88 | end
89 | end
90 |
91 | def for_commit(project, %{"branch" => branch, "head" => %{"id" => sha}})
92 | when is_binary(branch) and is_binary(sha) and byte_size(branch) > 0 and byte_size(sha) > 0 do
93 | Opencov.Build
94 | |> for_project(project.id)
95 | |> where([b], b.branch == ^branch and b.commit_sha == ^sha)
96 | end
97 | def for_commit(_, _), do: nil
98 |
99 | def compute_coverage(build) do
100 | build.jobs
101 | |> Enum.map(fn j -> j.coverage end)
102 | |> Enum.reject(fn n -> is_nil(n) or n == 0 end)
103 | |> Enum.min
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/test/controllers/project_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Opencov.ProjectControllerTest do
2 | use Opencov.ConnCase
3 |
4 | import Mock
5 |
6 | alias Opencov.Project
7 | @valid_attrs Map.take(params_for(:project), [:name, :base_url])
8 | @invalid_attrs %{name: nil}
9 |
10 | setup do
11 | conn = build_conn() |> with_login
12 | {:ok, conn: conn}
13 | end
14 |
15 | test "lists all entries on index", %{conn: conn} do
16 | conn = get conn, project_path(conn, :index)
17 | assert html_response(conn, 200) =~ "Projects"
18 | end
19 |
20 | test "renders form for new resources", %{conn: conn} do
21 | conn = get conn, project_path(conn, :new)
22 | assert html_response(conn, 200) =~ "project"
23 | end
24 |
25 | test "creates resource and redirects when data is valid", %{conn: conn} do
26 | conn = post conn, project_path(conn, :create), project: @valid_attrs
27 | project = Repo.get_by(Project, @valid_attrs)
28 | assert project
29 | assert redirected_to(conn) == project_path(conn, :show, project)
30 | end
31 |
32 | test "does not create resource and renders errors when data is invalid", %{conn: conn} do
33 | conn = post conn, project_path(conn, :create), project: %{}
34 | assert html_response(conn, 200) =~ "new"
35 | end
36 |
37 | test "shows chosen resource", %{conn: conn} do
38 | project = insert(:project, name: "name")
39 | conn = get conn, project_path(conn, :show, project)
40 | assert html_response(conn, 200) =~ project.name
41 | end
42 |
43 | test "renders page not found when id is nonexistent", %{conn: conn} do
44 | assert_raise Ecto.NoResultsError, fn ->
45 | get conn, project_path(conn, :show, -1)
46 | end
47 | end
48 |
49 | test "renders form for editing chosen resource", %{conn: conn} do
50 | project = insert(:project)
51 | conn = get conn, project_path(conn, :edit, project)
52 | assert html_response(conn, 200) =~ project.name
53 | end
54 |
55 | test "updates chosen resource and redirects when data is valid", %{conn: conn} do
56 | project = insert(:project)
57 | previous_token = project.token
58 | conn = put conn, project_path(conn, :update, project), project: @valid_attrs
59 | assert redirected_to(conn) == project_path(conn, :show, project)
60 | project = Repo.get_by(Project, @valid_attrs)
61 | assert project.token == previous_token
62 | end
63 |
64 | test "does not update chosen resource and renders errors when data is invalid", %{conn: conn} do
65 | project = insert(:project)
66 | conn = put conn, project_path(conn, :update, project), project: @invalid_attrs
67 | assert html_response(conn, 200) =~ project.name
68 | end
69 |
70 | test "deletes chosen resource", %{conn: conn} do
71 | project = insert(:project)
72 | conn = delete conn, project_path(conn, :delete, project)
73 | assert redirected_to(conn) == project_path(conn, :index)
74 | refute Repo.get(Project, project.id)
75 | end
76 |
77 | test "get badge", %{conn: conn} do
78 | project = insert(:project, current_coverage: nil)
79 | conn = get conn, project_badge_path(conn, :badge, project, "svg")
80 | assert conn.status == 200
81 | assert List.first(get_resp_header(conn, "content-type")) =~ "image/svg+xml"
82 | assert conn.resp_body =~ "NA"
83 |
84 | Repo.update! Ecto.Changeset.change(project, current_coverage: 80.0)
85 |
86 | conn = get conn, project_badge_path(conn, :badge, project, "svg")
87 | assert List.first(get_resp_header(conn, "content-type")) =~ "image/svg+xml"
88 | assert conn.resp_body =~ "80"
89 |
90 | with_mock Opencov.BadgeCreator, [make_badge: fn(_, _) -> {:ok, :png, "badge"} end] do
91 | conn = get conn, project_badge_path(conn, :badge, project, "png")
92 | assert List.first(get_resp_header(conn, "content-type")) =~ "image/png"
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/test/support/factory.ex:
--------------------------------------------------------------------------------
1 | defmodule Opencov.Factory do
2 | use ExMachina
3 | use ExMachina.EctoWithChangesetStrategy, repo: Opencov.Repo
4 |
5 | def project_factory do
6 | %Opencov.Project{
7 | name: sequence(:name, &("name-#{&1}")),
8 | base_url: sequence(:base_url, &("https://github.com/tuvistavie/name-#{&1}")),
9 | current_coverage: 50.0
10 | }
11 | end
12 |
13 | def settings_factory do
14 | %Opencov.Settings{
15 | signup_enabled: false,
16 | restricted_signup_domains: nil,
17 | default_project_visibility: "internal"
18 | }
19 | end
20 |
21 | def user_factory do
22 | %Opencov.User{
23 | id: sequence(:id, &(&1 + 2)),
24 | name: sequence(:name, &("name-#{&1}")),
25 | email: sequence(:email, &("email-#{&1}@example.com")),
26 | password: "my-secure-password"
27 | }
28 | end
29 |
30 | def build_factory do
31 | %Opencov.Build{
32 | build_number: sequence(:build_number, &(&1)),
33 | project: build(:project)
34 | }
35 | end
36 |
37 | def job_factory do
38 | %Opencov.Job{
39 | job_number: sequence(:job_number, &(&1)),
40 | build: build(:build)
41 | }
42 | end
43 |
44 | def file_factory do
45 | %Opencov.File{
46 | job: build(:job),
47 | name: sequence(:name, &("file-#{&1}")),
48 | source: "return 0",
49 | coverage_lines: []
50 | }
51 | end
52 |
53 | def badge_factory do
54 | %Opencov.Badge{
55 | project: build(:project),
56 | coverage: 50.0,
57 | image: "encoded_image",
58 | format: to_string(Opencov.Badge.default_format)
59 | }
60 | end
61 |
62 | def make_changeset(%Opencov.Project{} = project) do
63 | Opencov.ProjectManager.changeset(project, %{})
64 | end
65 |
66 | def make_changeset(%Opencov.File{} = file) do
67 | {job_id, file} = Map.pop(file, :job_id)
68 | job_id = job_id || file.job.id
69 | params = Map.from_struct(file)
70 | job = if job_id do
71 | Opencov.Repo.get(Opencov.Job, job_id)
72 | else
73 | insert(:job)
74 | end
75 | file = Ecto.build_assoc(job, :files)
76 | Opencov.FileManager.changeset(file, params)
77 | end
78 |
79 | def make_changeset(%Opencov.Build{} = build) do
80 | {project_id, build} = Map.pop(build, :project_id)
81 | project_id = project_id || build.project.id
82 | params = Map.from_struct(build)
83 | project = if project_id do
84 | Opencov.Repo.get(Opencov.Project, project_id)
85 | else
86 | insert(:project)
87 | end
88 | build = Ecto.build_assoc(project, :builds)
89 | Opencov.BuildManager.changeset(build, params)
90 | end
91 |
92 | def make_changeset(%Opencov.Job{} = job) do
93 | {build_id, job} = Map.pop(job, :build_id)
94 | build_id = build_id || job.build.id
95 | params = Map.from_struct(job)
96 | build = if build_id do
97 | Opencov.Repo.get(Opencov.Build, build_id)
98 | else
99 | insert(:build)
100 | end
101 | job = Ecto.build_assoc(build, :jobs)
102 | Opencov.JobManager.changeset(job, params)
103 | end
104 |
105 | def make_changeset(%Opencov.Badge{} = badge) do
106 | {project_id, badge} = Map.pop(badge, :project_id)
107 | params = Map.from_struct(badge)
108 | project = if project_id do
109 | Opencov.Repo.get(Opencov.Project, project_id)
110 | else
111 | insert(:project)
112 | end
113 | badge = Ecto.build_assoc(project, :badge)
114 | Opencov.BadgeManager.changeset(badge, params)
115 | end
116 |
117 | def make_changeset(model) do
118 | model
119 | end
120 |
121 | def with_project(build) do
122 | project = insert(:project)
123 | %{build | project_id: project.id}
124 | end
125 |
126 | def with_secure_password(user, password) do
127 | changeset = Opencov.UserManager.changeset(user, %{password: password})
128 | %{user | password_digest: changeset.changes[:password_digest]}
129 | end
130 |
131 | def confirmed_user(user) do
132 | %{user | confirmed_at: Timex.now, password_initialized: true}
133 | end
134 |
135 | def params_for(factory_name, attrs \\ %{}) do
136 | ExMachina.Ecto.params_for(__MODULE__, factory_name, attrs)
137 | |> Enum.reject(fn {_key, value} -> is_nil(value) or value == "" end)
138 | |> Enum.into(%{})
139 | end
140 | end
141 |
--------------------------------------------------------------------------------
/priv/static/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
45 |
50 |
51 |
53 |
54 |
56 | image/svg+xml
57 |
59 |
60 |
61 |
62 |
63 |
69 |
76 |
77 |
82 |
87 | C
99 | O
111 |
112 |
113 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenCov
2 |
3 | [](https://travis-ci.org/danhper/opencov)
4 | [](http://demo.opencov.com/projects/1)
5 |
6 | OpenCov is a self-hosted opensource test coverage history viewer.
7 | It is (mostly) compatible with [coveralls](https://coveralls.io/), so most
8 | coverage tools will work easily.
9 |
10 | ## Demo and screenshots
11 |
12 | A demo is available at http://demo.opencov.com, you can create an account or login with
13 |
14 | * username: user@opencov.com
15 | * password: password123
16 |
17 | For "security" reasons, the user is not admin.
18 | NOTE: the demo is on a Heroku free dyno, so it may not always be available and might be very slow.
19 |
20 | ### Projects list
21 |
22 | 
23 |
24 | ### Project page
25 |
26 | 
27 |
28 | ### Build page
29 |
30 | 
31 |
32 | ### Coverage page
33 |
34 | 
35 |
36 | ### Admin panel
37 |
38 | 
39 |
40 | ## Deploying the application
41 |
42 | ### Configuring
43 |
44 | First, you will need to at least setup a database
45 | To configure the app, create a `local.exs` file and override the configuration you need.
46 | Check [config/local.sample.exs](https://github.com/danhper/opencov/blob/master/config/local.sample.exs) to see the available configurations.
47 |
48 | ### Using docker
49 |
50 | #### With an existing database
51 |
52 | If you already have a database to use, you can simply start the application using docker:
53 |
54 | Setup database, run migrations and seeds
55 | ```
56 | $ docker run --rm -v /absolute/path/to/local.exs:/opencov/config/local.exs danhper/opencov mix ecto.setup
57 | ```
58 |
59 | Execute Phoenix Server
60 | ```
61 | $ docker run -v /absolute/path/to/local.exs:/opencov/config/local.exs danhper/opencov
62 | ```
63 |
64 | This will start the server on the port you set in `local.exs`.
65 |
66 | #### With docker-compose
67 |
68 | If you do not have a database, you can start one with `docker` and `docker-compose`. See [docker-compose.yml](https://github.com/danhper/opencov/blob/master/docker-compose.yml) for a sample `docker-compose.yml` file.
69 |
70 | Once you have your `docker-compose.yml` and `local.exs` ready, you can run
71 |
72 | ```
73 | $ docker-compose run opencov mix ecto.setup
74 | $ docker-compose up
75 | ```
76 |
77 | ### Manually
78 |
79 | ```
80 | $ git clone https://github.com/danhper/opencov.git
81 | $ cd opencov
82 | $ cp /path/to/local.exs config/local.exs # local.exs must be in the `config` directory of the app
83 |
84 | $ npm install # (or yarn install)
85 | $ mix deps.get
86 | $ mix ecto.setup
87 | $ mix phoenix.server
88 | ```
89 |
90 | This should start OpenCov at port 4000.
91 |
92 | If you want to setup the server for production, you will need to run the above commands
93 | with `MIX_ENV=prod` and to run
94 |
95 | ```
96 | $ mix assets.compile
97 | ```
98 |
99 | before starting the server.
100 |
101 | ### Deploying to Heroku
102 |
103 | You should also be able to deploy to Heroku by simply git pushing this repository.
104 | You will need to set the following environment variables using `heroku config:set`
105 |
106 | * `OPENCOV_PORT`
107 | * `OPENCOV_SCHEME`
108 | * `SECRET_KEY_BASE`
109 | * `SMTP_USER`
110 | * `SMTP_PASSWORD`
111 |
112 | You will need to run
113 |
114 | ```
115 | $ heroku run mix ecto.setup
116 | ```
117 |
118 | before you can use your application.
119 |
120 | ### Default user
121 |
122 | In all setups, `mix ecto.setup` creates the following admin user
123 |
124 | * email: admin@example.com
125 | * password: p4ssw0rd
126 |
127 | You should use it for your first login and the change the email and password.
128 |
129 | ## Sending test metrics
130 |
131 | A few languages are documented in [the wiki](https://github.com/danhper/opencov/wiki).
132 | For other languages, coveralls instructions should work out of the box,
133 | you just need to set the URL to your OpenCov server and to explicitly set
134 | the token, even when using Travis.
135 |
136 | ## Development status
137 |
138 | The application is more or less stable. I have been using it
139 | for a little while now with coverage data from the 4 languages in the Wiki.
140 |
141 | The main missing feature is the ability to send coverage status on pull requests.
142 | The implementation is started in the [integrations branch](https://github.com/danhper/opencov/tree/integrations) but I could not find the time to finish it yet.
143 |
144 | I am open to any other suggestions, and help is very welcome.
145 |
--------------------------------------------------------------------------------