├── .dockerignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── compile ├── config ├── config.exs ├── dev.exs ├── local.sample.exs ├── prod.exs └── test.exs ├── cover.sh ├── docker-compose.yml ├── elixir_buildpack.config ├── eyecatch.rb ├── lib ├── opencov.ex └── opencov │ ├── authentication.ex │ ├── badge_creator.ex │ ├── core.ex │ ├── endpoint.ex │ ├── helpers │ ├── authentication.ex │ ├── datetime.ex │ └── display.ex │ ├── imagemagick.ex │ ├── mailer.ex │ ├── plug │ ├── anonymous_only.ex │ ├── authentication.ex │ ├── fetch_user.ex │ └── force_password_initialize.ex │ ├── repo.ex │ ├── templates │ └── badge_template.eex │ └── types │ └── json.ex ├── mix.exs ├── mix.lock ├── package.json ├── phoenix_static_buildpack.config ├── priv ├── gettext │ └── en │ │ └── LC_MESSAGES │ │ ├── errors.po │ │ ├── project.po │ │ ├── settings.po │ │ └── user.po ├── repo │ ├── migrations │ │ ├── 20151022125349_create_project.exs │ │ ├── 20151022141210_create_build.exs │ │ ├── 20151022143636_create_job.exs │ │ ├── 20151022144345_create_file.exs │ │ ├── 20151108002255_create_badge.exs │ │ ├── 20151202133406_create_user.exs │ │ └── 20151213015301_create_settings.exs │ └── seeds │ │ ├── 0001_settings.exs │ │ ├── 0002_admin.exs │ │ └── eyecatch │ │ ├── 0010_projects.exs │ │ ├── 0011_builds.exs │ │ ├── 0012_jobs.exs │ │ └── 0013_files.exs └── static │ ├── favicon.ico │ ├── images │ └── logo.svg │ └── robots.txt ├── test ├── controllers │ ├── api │ │ └── v1 │ │ │ └── job_controller_test.exs │ ├── build_controller_test.exs │ ├── file_controller_test.exs │ ├── job_controller_test.exs │ └── project_controller_test.exs ├── fixtures │ └── dummy-coverages.json ├── lib │ ├── badge_creator_test.exs │ └── types │ │ └── json_test.exs ├── managers │ ├── badge_manager_test.exs │ ├── build_manager_test.exs │ ├── file_manager_test.exs │ ├── job_manager_test.exs │ ├── project_manager_test.exs │ ├── settings_manager_test.exs │ └── user_manager_test.exs ├── models │ ├── badge_test.exs │ ├── build_test.exs │ ├── file_test.exs │ ├── job_test.exs │ ├── project_test.exs │ ├── settings_test.exs │ └── user_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ ├── ecto_with_changeset_stragegy.ex │ ├── factory.ex │ ├── fixtures.ex │ ├── manager_case.ex │ └── model_case.ex ├── test_helper.exs └── views │ ├── common_view_test.exs │ ├── error_view_test.exs │ ├── file_view_test.exs │ ├── job_view_test.exs │ └── layout_view_test.exs ├── web ├── controllers │ ├── admin │ │ ├── dashboard_controller.ex │ │ ├── project_controller.ex │ │ ├── settings_controller.ex │ │ └── user_controller.ex │ ├── api │ │ └── v1 │ │ │ └── job_controller.ex │ ├── auth_controller.ex │ ├── build_controller.ex │ ├── file_controller.ex │ ├── job_controller.ex │ ├── profile_controller.ex │ ├── project_controller.ex │ └── user_controller.ex ├── gettext.ex ├── mailers │ ├── app_mailer.ex │ └── user_mailer.ex ├── managers │ ├── badge_manager.ex │ ├── build_manager.ex │ ├── file_manager.ex │ ├── job_manager.ex │ ├── project_manager.ex │ ├── settings_manager.ex │ └── user_manager.ex ├── models │ ├── badge.ex │ ├── build.ex │ ├── file.ex │ ├── job.ex │ ├── project.ex │ ├── settings.ex │ └── user.ex ├── router.ex ├── services │ ├── file_service.ex │ └── user_service.ex ├── static │ ├── css │ │ ├── main.styl │ │ ├── pages │ │ │ ├── admin.styl │ │ │ ├── build.styl │ │ │ ├── files.styl │ │ │ ├── project.styl │ │ │ └── user.styl │ │ ├── partials │ │ │ ├── base.styl │ │ │ ├── breadcrumb.styl │ │ │ ├── global.styl │ │ │ ├── header.styl │ │ │ └── util.styl │ │ └── theme.less │ └── js │ │ ├── app.js │ │ └── components │ │ ├── badges │ │ ├── badges.jade │ │ ├── badges.styl │ │ └── index.js │ │ ├── click-safe.js │ │ ├── file-coverage │ │ ├── file-coverage.jade │ │ ├── file-coverage.styl │ │ └── index.js │ │ ├── index.js │ │ ├── project-token │ │ ├── index.js │ │ ├── project-token.jade │ │ └── project-token.styl │ │ ├── selectable │ │ ├── index.js │ │ └── selectable.styl │ │ └── util │ │ ├── index.js │ │ └── raw.js ├── templates │ ├── admin │ │ ├── dashboard │ │ │ └── index.html.eex │ │ ├── project │ │ │ ├── index.html.eex │ │ │ └── show.html.eex │ │ ├── settings │ │ │ └── edit.html.eex │ │ ├── shared │ │ │ └── actions.html.eex │ │ └── user │ │ │ ├── edit.html.eex │ │ │ ├── index.html.eex │ │ │ ├── new.html.eex │ │ │ └── show.html.eex │ ├── auth │ │ └── login.html.eex │ ├── build │ │ ├── commit.html.eex │ │ └── show.html.eex │ ├── file │ │ ├── list.html.eex │ │ └── show.html.eex │ ├── job │ │ └── show.html.eex │ ├── layout │ │ ├── app.html.eex │ │ └── header.html.eex │ ├── mailers │ │ └── user │ │ │ ├── confirmation.html.eex │ │ │ ├── confirmation.text.eex │ │ │ ├── reset_password.html.eex │ │ │ └── reset_password.text.eex │ ├── profile │ │ ├── edit.html.eex │ │ ├── edit_password.html.eex │ │ ├── password_form.html.eex │ │ ├── reset_password.html.eex │ │ └── reset_password_request.html.eex │ ├── project │ │ ├── edit.html.eex │ │ ├── form.html.eex │ │ ├── index.html.eex │ │ ├── new.html.eex │ │ └── show.html.eex │ ├── shared │ │ └── coverage_diff.html.eex │ └── user │ │ ├── form.html.eex │ │ └── new.html.eex ├── views │ ├── admin │ │ ├── dashboard_view.ex │ │ ├── project_view.ex │ │ ├── settings_view.ex │ │ ├── shared_view.ex │ │ └── user_view.ex │ ├── api │ │ └── v1 │ │ │ └── job_view.ex │ ├── auth_view.ex │ ├── build_view.ex │ ├── common_view.ex │ ├── error_helpers.ex │ ├── error_view.ex │ ├── file_view.ex │ ├── form_helpers.ex │ ├── job_view.ex │ ├── layout_view.ex │ ├── profile_view.ex │ ├── project_view.ex │ ├── shared_view.ex │ └── user_view.ex └── web.ex ├── webpack.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | _build 2 | deps 3 | node_modules 4 | .git 5 | .tags 6 | postgresql-data 7 | priv/static/js 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenCov 2 | 3 | [![Build Status](https://travis-ci.org/danhper/opencov.svg?branch=master)](https://travis-ci.org/danhper/opencov) 4 | [![Coverage Status](http://demo.opencov.com/projects/1/badge.svg)](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 | ![projects](https://cloud.githubusercontent.com/assets/1436271/21740030/45ce95d6-d4ef-11e6-8d09-fac4aa7d5f00.png) 23 | 24 | ### Project page 25 | 26 | ![project page](https://cloud.githubusercontent.com/assets/1436271/21740031/45d0bafa-d4ef-11e6-93dc-0decbbd1d973.png) 27 | 28 | ### Build page 29 | 30 | ![build page](https://cloud.githubusercontent.com/assets/1436271/21740029/45cd825e-d4ef-11e6-9a55-ab19be6a3690.png) 31 | 32 | ### Coverage page 33 | 34 | ![coverage page](https://cloud.githubusercontent.com/assets/1436271/21740028/45cca55a-d4ef-11e6-9515-6b8672549dbd.png) 35 | 36 | ### Admin panel 37 | 38 | ![admin panel](https://cloud.githubusercontent.com/assets/1436271/21740375/adaaaa08-d4fb-11e6-916b-439a2eaeeb3b.png) 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 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "stack": "heroku-20" 3 | } 4 | -------------------------------------------------------------------------------- /compile: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/webpack -p 2 | mix phx.digest 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /lib/opencov/templates/badge_template.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | coverage 7 | coverage 8 | <%= coverage_str %> 9 | <%= coverage_str %> 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danhper/opencov/d3983d5d0352545e39167ce1f971c713651aa7d3/priv/static/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/models/project_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Opencov.ProjectTest do 2 | use Opencov.ModelCase 3 | 4 | end 5 | -------------------------------------------------------------------------------- /test/models/settings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Opencov.SettingsTest do 2 | use Opencov.ModelCase 3 | end 4 | -------------------------------------------------------------------------------- /test/models/user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Opencov.UserTest do 2 | use Opencov.ModelCase 3 | end 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Opencov.LayoutViewTest do 2 | use Opencov.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/static/css/main.styl: -------------------------------------------------------------------------------- 1 | @import "nib" 2 | 3 | @import "./partials/*" 4 | @import "./pages/*" 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/css/pages/files.styl: -------------------------------------------------------------------------------- 1 | .files 2 | table 3 | th, td 4 | &:first-child, &:nth-child(2) 5 | width 120px 6 | -------------------------------------------------------------------------------- /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/static/css/pages/user.styl: -------------------------------------------------------------------------------- 1 | .user-form 2 | .admin.form-group 3 | margin 2em 0 4 | user-select none 5 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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="[![Coverage Status]({ badgeURL })]({ opts.projectUrl })" rows="3") 13 | dt HTML 14 | dd 15 | selectable( 16 | text="\"Coverage" 17 | rows="4" 18 | ) 19 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/static/js/components/util/index.js: -------------------------------------------------------------------------------- 1 | import './raw' 2 | -------------------------------------------------------------------------------- /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/templates/admin/dashboard/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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/templates/admin/project/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <%= for project <- @projects do %> 25 | 26 | 27 | 28 | 29 | 33 | 34 | <% end %> 35 | 36 |
<%= @paginator.total_entries %> total projects
NameURLCreated on
<%= link project.name, to: admin_project_path(@conn, :show, project) %><%= link project.base_url, to: project.base_url, target: "_blank" %><%= Datetime.format(project.inserted_at, :dateonly) %> 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 |
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 | -------------------------------------------------------------------------------- /web/templates/admin/project/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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 | -------------------------------------------------------------------------------- /web/templates/admin/settings/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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 | -------------------------------------------------------------------------------- /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/templates/admin/user/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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/templates/admin/user/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <%= for user <- @users do %> 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | <% end %> 37 | 38 |
<%= @paginator.total_entries %> total users
NameEmailConfirmedAdmin
<%= link user.name, to: admin_user_path(@conn, :show, user) %><%= user.email %><%= Display.bool(!is_nil(user.confirmed_at)) %><%= Display.bool(user.admin) %> 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 |
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/user/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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/templates/admin/user/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/build/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | <%= for job <- @build.jobs do %> 43 | 44 | 45 | 48 | 53 | 54 | 55 | 56 | <% end %> 57 | 58 |
NumberCoverageDiffRun timeFiles count
<%= link "##{job.job_number}", to: job_path(@conn, :show, job) %> 46 | <%= format_coverage(job.coverage) %> 47 | 49 | <%= if job.previous_coverage do %> 50 | <%= render Opencov.SharedView, "coverage_diff.html", diff: job.coverage - job.previous_coverage %> 51 | <% end %> 52 | <%= job |> Opencov.JobView.job_time |> human_time_ago %><%= job.files_count %>
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/templates/file/list.html.eex: -------------------------------------------------------------------------------- 1 |
2 |

Files (<%= @paginator.total_entries %>)

3 | 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 | 31 | <% end %> 32 | 33 | 34 | 35 | <%= for file <- @files do %> 36 | 37 | 40 | 45 | 46 | 47 | <% end %> 48 | 49 |
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 |
38 | <%= format_coverage(file.coverage) %> 39 | 41 | <%= if file.previous_coverage do %> 42 | <%= render Opencov.SharedView, "coverage_diff.html", diff: file.coverage - file.previous_coverage %> 43 | <% end %> 44 | <%= link file.name, to: file_path(@conn, :show, file) %>
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 | -------------------------------------------------------------------------------- /web/templates/file/show.html.eex: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /web/templates/job/show.html.eex: -------------------------------------------------------------------------------- 1 | 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/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 | 24 | <% end %> 25 | <%= if get_flash(@conn, :error) do %> 26 | 27 | <% end %> 28 |
29 | 30 |
31 | <%= @inner_content %> 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/templates/layout/header.html.eex: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/templates/profile/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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/templates/profile/edit_password.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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/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 | -------------------------------------------------------------------------------- /web/templates/profile/reset_password.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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 | -------------------------------------------------------------------------------- /web/templates/profile/reset_password_request.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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/templates/project/edit.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 11 | 12 | <%= render "form.html", changeset: @changeset, 13 | action: project_path(@conn, :update, @project) %> 14 | 15 |
16 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /web/templates/project/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 11 | 12 | <%= render "form.html", changeset: @changeset, 13 | action: project_path(@conn, :create) %> 14 |
15 | -------------------------------------------------------------------------------- /web/templates/project/show.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 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 |
22 | 23 |
24 | 25 | 36 |
37 | 38 |

Recent builds

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | <%= for build <- @project.builds do %> 55 | 56 | 57 | 58 | 61 | 66 | 67 | 68 | 69 | 70 | 71 | <% end %> 72 | 73 |
BuildBranchCoverageDiffCommitCommitterTimeVia
#<%= build.build_number %><%= build.branch %> 59 | <%= format_coverage(build.coverage) %> 60 | 62 | <%= if build.previous_coverage do %> 63 | <%= render Opencov.SharedView, "coverage_diff.html", diff: build.coverage - build.previous_coverage %> 64 | <% end %> 65 | <%= render Opencov.BuildView, "commit.html", build: %{build|project: @project} %><%= build.committer_name %><%= human_time_ago(build.inserted_at) %><%= build.service_name %>
74 |
75 | -------------------------------------------------------------------------------- /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/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/templates/user/new.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 | 9 | <%= render "form.html", changeset: @changeset, 10 | action: user_path(@conn, :create), 11 | show_password: true, 12 | is_admin: false %> 13 |
14 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/views/profile_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Opencov.ProfileView do 2 | use Opencov.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule Opencov.UserView do 2 | use Opencov.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------