├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── bin
└── deploy.sh
├── docker-compose.override.yml
├── docker-compose.prod.yml
├── docker-compose.yml
├── end-of-life
├── badge.svg
├── index.html
└── static
│ ├── favicon.png
│ ├── logo_navbar.png
│ └── style.css
├── functional_tests
├── .env
├── .gitignore
├── .out
├── README.md
├── conftest.py
├── pytest.ini
├── requirements.txt
├── selenium_tests
│ ├── conftest.py
│ ├── test_browsing.py
│ └── test_pages.py
└── stateless_tests
│ ├── test_badge.py
│ └── test_error_pages.py
├── graphics
├── badge_demo.xcf
├── logo_bright.xcf
├── logo_dark.xcf
└── logo_navbar.xcf
└── www
├── Dockerfile
├── config
├── config.exs
├── dev.exs
├── prod.exs
└── test.exs
├── lib
├── krihelinator.ex
├── krihelinator
│ ├── github.ex
│ ├── github
│ │ ├── language.ex
│ │ └── repo.ex
│ ├── history.ex
│ ├── history
│ │ └── language.ex
│ ├── import_export.ex
│ ├── periodic.ex
│ ├── periodic
│ │ ├── big_query.ex
│ │ └── github_trending.ex
│ ├── repo.ex
│ ├── scraper.ex
│ ├── web.ex
│ └── web
│ │ ├── controllers
│ │ ├── badge_controller.ex
│ │ ├── data_controller.ex
│ │ └── page_controller.ex
│ │ ├── endpoint.ex
│ │ ├── input_validator.ex
│ │ ├── router.ex
│ │ ├── templates
│ │ ├── badge
│ │ │ └── badge.svg.eex
│ │ ├── layout
│ │ │ ├── app.html.eex
│ │ │ ├── navbar.html.eex
│ │ │ └── navbar_link.html.eex
│ │ └── page
│ │ │ ├── about.html.eex
│ │ │ ├── badge.html.eex
│ │ │ ├── language.html.eex
│ │ │ ├── languages.html.eex
│ │ │ ├── languages_history.html.eex
│ │ │ ├── repo_item.html.eex
│ │ │ ├── repositories.html.eex
│ │ │ └── repository.html.eex
│ │ └── views
│ │ ├── badge_view.ex
│ │ ├── error_view.ex
│ │ ├── layout_view.ex
│ │ └── page_view.ex
└── mix
│ └── tasks
│ └── krihelinator.import.ex
├── mix.exs
├── mix.lock
├── priv
├── repo
│ ├── migrations
│ │ ├── 20160707200816_create_github_repo.exs
│ │ ├── 20160714142255_repo_name_instead_of_user_and_repo_fields.exs
│ │ ├── 20160714153524_add_krihelimeter_field.exs
│ │ ├── 20160716170829_add_description_field.exs
│ │ ├── 20160721085732_add_authors_field.exs
│ │ ├── 20160722120749_add_trending_field.exs
│ │ ├── 20160728094627_add_user_requested_field.exs
│ │ ├── 20160808171600_add_language_field.exs
│ │ ├── 20161219220300_create_language_history.exs
│ │ ├── 20161220021211_add_forks_field.exs
│ │ ├── 20161225142125_create_language.exs
│ │ ├── 20161225144134_use_new_language_model.exs
│ │ ├── 20161225205601_remove_the_repo_language_name_field.exs
│ │ ├── 20170204024823_add_dirty_bit.exs
│ │ ├── 20170218222859_create_showcase.exs
│ │ ├── 20170219105003_add_showcase_description.exs
│ │ ├── 20170411165434_unlimit_description_length.exs
│ │ ├── 20170417211703_remove_num_of_repos.exs
│ │ └── 20171104012807_remove_showcases.exs
│ └── seeds.exs
├── scripts
│ ├── delete_zero_commits_repos.exs
│ ├── remove_duplicate_history.exs
│ ├── repos_to_csv.exs
│ └── update_krihelimeter.exs
└── static
│ ├── css
│ └── style.css
│ ├── favicon.png
│ ├── js
│ └── app.js
│ ├── loaderio-af9cda539c1b3a4a235147af21f0fe5d.txt
│ ├── media
│ ├── badge_demo.png
│ └── logo_navbar.png
│ └── robots.txt
└── test
├── controllers
├── badge_controller_test.exs
└── page_controller_test.exs
├── fixtures
└── dump_sample.json
├── github
├── language_test.exs
└── repo_test.exs
├── history
└── language_test.exs
├── import_export_test.exs
├── input_validator_test.exs
├── periodic
└── github_trending_test.exs
├── scraper_test.exs
├── support
├── conn_case.ex
└── model_case.ex
├── test_helper.exs
└── views
├── error_view_test.exs
├── layout_view_test.exs
└── page_view_test.exs
/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | _build
3 | deps
4 | .deliver/releases
5 |
6 | # Generated on crash by the VM
7 | erl_crash.dump
8 |
9 | # Dont track my secrets
10 | secrets/
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 |
3 | services:
4 | - docker
5 |
6 | env:
7 | - DOCKER_COMPOSE_VERSION=1.18.0
8 |
9 | before_install:
10 | - sudo rm /usr/local/bin/docker-compose
11 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
12 | - chmod +x docker-compose
13 | - sudo mv docker-compose /usr/local/bin
14 |
15 | before_script:
16 | - docker-compose build
17 | - docker-compose run -e MIX_ENV=test www mix do deps.get, compile
18 |
19 | script:
20 | - docker-compose run -e MIX_ENV=test www mix test
21 | - docker-compose run -e MIX_ENV=test www mix credo
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Tom Gurion
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 | # The Krihelinator
2 |
3 | :warning: **This project reached its end-of-life! The info here is only for documentation. The project was shut down on April 2020. On July 2020 the [krihelinator.xyz](https://krihelinator.xyz) domain will stop working and this page will only be available on [nagasaki45.github.io/krihelinator](https://nagasaki45.github.io/krihelinator).**
4 |
5 | ## About
6 |
7 | > *"Trendiness of open source software should be assessed by contribution rate, not by stars"*
8 | >
9 | > \- Meir Kriheli
10 |
11 | This project proposes an alternative to github's [trending page](http://github.com/trending), by exposing projects with high contribution rate, instead of daily stars.
12 | The krihelimeter of each repository is calculated using the commits, pull requests, and issues of that project, from the past week (based on github's pulse page).
13 |
14 |
15 |
16 |
Krihelimeter =
17 |
20
18 |
* authors +
19 |
20 |
21 |
22 |
8
23 |
* merged and proposed pull requests +
24 |
25 |
26 |
27 |
8
28 |
* new and closed issues +
29 |
30 |
31 |
32 |
1
33 |
* commits
34 |
35 |
36 |
37 | During the development of this project I found out that people use github as a backup service, automating hundreds of commits per week.
38 | Therefor, to filter these projects out, only projects with more than one author enters the Krihelinator DB.
39 |
40 | Drop me a line at nagasaki45@gmail.com if you do somethig interesting with this project. Will be happy to hear about it and might be able to help.
41 |
42 | ## About the shutdown
43 |
44 | - On January 2019 github changed the way repo's pulse page is loading. Instead of having the entire HTML available at once, some info was fetched in subsequent calls. Specifically, the number of commits and authors were missing from the pulse page. This broke the calculation of the krihelimeter. Note that although the krihelimeter calculation was now different than the one intended, the information presented on the krihelinator was still relevant because it affected all projects / languages in the same way.
45 | - By the end of March 2020 github started to block scrapers like the krihelinator, returning HTTP error code 429 (Too Many Requests).
46 | - On early April 2020 I've decided to shutdown the project. The [krihelinator.xyz](https://krihelinator.xyz) domain now points to a page saying that the project is down. Links to badges return an end-of-life (EOL) badge.
47 | - On July 2020 the domain shut down. The end-of-life remained available on [nagasaki45.github.io/krihelinator](https://nagasaki45.github.io/krihelinator).
48 |
49 | ## Similar projects
50 |
51 | - [GitHut](http://githut.info/) and [GitHut 2](https://madnight.github.io/githut/)
52 | - [IsItMaintained](http://isitmaintained.com/)
53 | - [GitHub profiler](http://www.datasciencecentral.com/profiles/blogs/github-profiler-a-tool-for-repository-evaluation)
54 |
--------------------------------------------------------------------------------
/bin/deploy.sh:
--------------------------------------------------------------------------------
1 | docker-compose build www
2 | docker tag krihelinator_www nagasaki45/krihelinator_www
3 | docker push nagasaki45/krihelinator_www
4 |
5 | scp docker-compose.yml krihelinator.xyz:krihelinator/
6 | scp docker-compose.prod.yml krihelinator.xyz:krihelinator/
7 | scp -r secrets krihelinator.xyz:krihelinator/
8 |
9 | ssh krihelinator.xyz << EOF
10 | cd krihelinator
11 | docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull
12 | docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
13 | EOF
14 |
--------------------------------------------------------------------------------
/docker-compose.override.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | www:
5 | environment:
6 | - MIX_ENV=dev
7 | build:
8 | context: './www'
9 | dockerfile: 'Dockerfile'
10 | ports:
11 | - 4000:4000
12 | volumes:
13 | - ./www:/home/elixir/app
14 |
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | db:
5 | restart: unless-stopped
6 | www:
7 | image: nagasaki45/krihelinator_www
8 | restart: unless-stopped
9 | environment:
10 | - MIX_ENV=prod
11 | ports:
12 | - 443:4040
13 | - 80:4000
14 | volumes:
15 | - /etc/letsencrypt:/etc/letsencrypt
16 | - ./webroot:/webroot
17 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | db:
5 | image: 'postgres'
6 | www:
7 | links:
8 | - db
9 | volumes:
10 | - ./secrets:/secrets
11 |
--------------------------------------------------------------------------------
/end-of-life/badge.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/end-of-life/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | the Krihelinator
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
29 |
30 |
31 |
"Trendiness of open source software should be assessed by contribution rate, not by stars"
32 |
- Meir Kriheli
33 |
34 |
35 |
36 |
This project reached its end-of-life
37 |
38 | The info here is only for documentation. The project was shut down on April 2020. On July 2020 the krihelinator.xyz domain will stop working and this page will only be available on nagasaki45.github.io/krihelinator. More info about the shut down of is available here.
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/end-of-life/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagasaki45/krihelinator/243bfe476b8128dc2f0fcd913bebd8cf20b7deb6/end-of-life/static/favicon.png
--------------------------------------------------------------------------------
/end-of-life/static/logo_navbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagasaki45/krihelinator/243bfe476b8128dc2f0fcd913bebd8cf20b7deb6/end-of-life/static/logo_navbar.png
--------------------------------------------------------------------------------
/end-of-life/static/style.css:
--------------------------------------------------------------------------------
1 | /* For fixed navbar */
2 | body {
3 | padding-top: 80px;
4 | }
5 |
6 | .my-navbar-header {
7 | min-height: 60px;
8 | }
9 |
10 | nav img {
11 | height: 42px;
12 | margin-top: 10px;
13 | }
14 |
15 | /* Costumizations for desktop. Mobile first! */
16 | @media (min-width: 768px) {
17 | .container {
18 | max-width: 850px;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/functional_tests/.env:
--------------------------------------------------------------------------------
1 | source env/bin/activate
2 |
--------------------------------------------------------------------------------
/functional_tests/.gitignore:
--------------------------------------------------------------------------------
1 | env
2 | __pycache__
3 | .cache
4 | geckodriver.log
5 |
--------------------------------------------------------------------------------
/functional_tests/.out:
--------------------------------------------------------------------------------
1 | deactivate
2 |
--------------------------------------------------------------------------------
/functional_tests/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to the Krihelinator functional tests project
2 |
3 | This project is a set of functional tests for the Krihelinator, writen with python3.6, selenium (using the Firefox driver), requests, and pytest.
4 | Although it currently reside in the same repo, this project doesn't share dependencies with the rest of the Krihelinator code.
5 |
6 | ## Getting started
7 |
8 | ```bash
9 | python3.6 -m venv env # Using virtual env is highly recommended
10 | source env/bin/activate
11 | pip install -r requirements.text
12 | # Make sure that selenium dependencies are install properly!!!
13 | ```
14 |
15 | ## Running the tests
16 |
17 | ```bash
18 | # Locally
19 | pytest
20 | # Or against production with
21 | BASE_URL='http://www.krihelinator.xyz' pytest
22 | ```
23 |
--------------------------------------------------------------------------------
/functional_tests/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 |
6 | @pytest.fixture(scope='session')
7 | def base_url():
8 | default = 'http://localhost:4000'
9 | return os.environ.get('BASE_URL', default)
10 |
--------------------------------------------------------------------------------
/functional_tests/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | norecursedirs = env
3 | addopts = -s
4 |
--------------------------------------------------------------------------------
/functional_tests/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi==2018.10.15
2 | chardet==3.0.4
3 | decorator==4.0.11
4 | idna==2.7
5 | ipdb==0.10.1
6 | ipython==5.1.0
7 | ipython-genutils==0.1.0
8 | pexpect==4.2.1
9 | pickleshare==0.7.4
10 | prompt-toolkit==1.0.9
11 | ptyprocess==0.5.1
12 | py==1.4.32
13 | Pygments==2.1.3
14 | pytest==3.0.5
15 | requests==2.20.0
16 | selenium==3.4.0
17 | simplegeneric==0.8.1
18 | six==1.10.0
19 | traitlets==4.3.1
20 | urllib3==1.24.1
21 | wcwidth==0.1.7
22 |
--------------------------------------------------------------------------------
/functional_tests/selenium_tests/conftest.py:
--------------------------------------------------------------------------------
1 | import time
2 | import urllib
3 |
4 | import pytest
5 | from selenium import webdriver
6 |
7 |
8 | class Driver(webdriver.Firefox):
9 | """
10 | Extended Firefox driver.
11 | """
12 |
13 | def wait_for(self, function, timeout=10):
14 | """
15 | Wait for something to happen. When timeout expires assert.
16 | """
17 |
18 | for attempt in range(timeout):
19 | if function():
20 | break
21 | time.sleep(1)
22 | else:
23 | assert function()
24 |
25 | def assert_in_current_title(self, title):
26 | """
27 | Wait for the page title, making sure we are on the right page.
28 | """
29 | self.wait_for(lambda: title in self.title)
30 |
31 | def assert_current_url(self, url):
32 | """
33 | Waits for the given URL to load. Raise if different URL found after
34 | timeout.
35 | """
36 | self.wait_for(
37 | lambda: urllib.parse.unquote(self.current_url).strip('/') == url
38 | )
39 |
40 |
41 | @pytest.fixture(scope='session')
42 | def driver():
43 | driver = Driver()
44 | yield driver
45 | driver.quit()
46 |
--------------------------------------------------------------------------------
/functional_tests/selenium_tests/test_browsing.py:
--------------------------------------------------------------------------------
1 | """
2 | Make sure that the site is browsable: clicking things leads to the right places.
3 | """
4 |
5 |
6 | def go_to(driver, navbar_link):
7 | navbar = driver.find_element_by_tag_name('nav')
8 | if navbar_link == 'logo':
9 | navbar.find_element_by_tag_name('img').click()
10 | else:
11 | navbar.find_element_by_link_text(navbar_link).click()
12 |
13 |
14 | def test_browsing(driver, base_url):
15 | driver.get(base_url)
16 |
17 | go_to(driver, 'About')
18 | driver.assert_current_url(base_url + '/about')
19 | go_to(driver, 'logo')
20 | driver.assert_current_url(base_url)
21 | go_to(driver, 'Languages')
22 | driver.assert_current_url(base_url + '/languages')
23 |
24 | # From the languages page select a language and proceed to the history
25 | # page
26 | tbody = driver.find_element_by_tag_name('tbody')
27 | language = tbody.find_element_by_tag_name('tr') # The 1st lang
28 | language_name = language.find_elements_by_tag_name('td')[1].text
29 | language.find_element_by_tag_name('input').click()
30 |
31 | # Selenium doesn't know how to click on things that were manipulated
32 | # using JS. The solution: click using JS script.
33 | button = driver.find_element_by_css_selector('button.btn-primary')
34 | driver.execute_script('arguments[0].click();', button)
35 |
36 | url_params = f'languages=["{language_name}"]'
37 | driver.assert_current_url(base_url + '/languages-history?' + url_params)
38 |
39 | # Going back to languages to select one
40 | go_to(driver, 'Languages')
41 | language_name = 'Python'
42 | driver.assert_current_url(base_url + '/languages')
43 | tbody = driver.find_element_by_tag_name('tbody')
44 | tbody.find_element_by_link_text(language_name).click()
45 | driver.assert_current_url(f'{base_url}/languages/{language_name}')
46 |
47 | # Click on the "show history" button
48 | driver.find_element_by_link_text('Show language history').click()
49 | url_params = f'languages=["{language_name}"]'
50 | driver.assert_current_url(base_url + '/languages-history?' + url_params)
51 |
--------------------------------------------------------------------------------
/functional_tests/selenium_tests/test_pages.py:
--------------------------------------------------------------------------------
1 | """
2 | Basic tests check that all of the pages are there and presenting the basic
3 | information properly.
4 | """
5 |
6 | import random
7 |
8 | import requests
9 |
10 |
11 | def validate_page_contains_list_of_repositories(driver):
12 | list_element = driver.find_element_by_css_selector('.list-group')
13 | repos = list_element.find_elements_by_tag_name('li')
14 | assert len(repos) > 0
15 | assert len(repos) <= 50
16 |
17 | # Examine a random repo
18 | repo = random.choice(repos)
19 | title = repo.find_element_by_tag_name('h3').text
20 | assert '/' in title
21 | krihelimeter_badge = repo.find_element_by_tag_name('svg')
22 | assert 'Krihelimeter' in krihelimeter_badge.text
23 |
24 |
25 | def test_homepage(driver, base_url):
26 | driver.get(base_url)
27 |
28 | # Basic page properties
29 | assert 'krihelinator' in driver.title.lower()
30 | quote = driver.find_element_by_tag_name('blockquote')
31 | assert 'Trendiness of open source software should be assessed by' in quote.text
32 |
33 | validate_page_contains_list_of_repositories(driver)
34 |
35 |
36 | def test_languages(driver, base_url):
37 | driver.get(base_url + '/languages')
38 |
39 | # The languages table is shown
40 | table = driver.find_element_by_tag_name('table')
41 |
42 | # With correct headers
43 | thead = table.find_element_by_tag_name('thead')
44 | ths = thead.find_elements_by_tag_name('th')
45 | expecteds = ['#', 'Language', 'Krihelimeter', 'Select']
46 | for th, expected in zip(ths, expecteds):
47 | assert th.text == expected
48 |
49 | # Make sure JavaScript and Python are there
50 | table_text = table.text
51 | assert 'Python' in table_text
52 | assert 'JavaScript' in table_text
53 |
54 |
55 | def test_about(driver, base_url):
56 | driver.get(base_url + '/about')
57 |
58 | # Inspect the text a bit
59 | text = driver.find_element_by_tag_name('main').text
60 | assert "alternative to github's trending" in text
61 |
62 |
63 | def test_language(driver, base_url):
64 | driver.get(base_url + '/languages/Python')
65 |
66 | # The language name and summarized stats are there
67 | language = driver.find_element_by_css_selector('.header')
68 | language_name = language.find_element_by_tag_name('h1').text
69 | assert language_name == 'Python'
70 | language_stats = language.find_element_by_tag_name('h4').text
71 | assert 'Total Krihelimeter' in language_stats
72 |
73 | validate_page_contains_list_of_repositories(driver)
74 |
75 |
76 | def test_language_that_requires_encoding(driver, base_url):
77 | """
78 | Make sure the language page and history page for languages like C#
79 | are working. Bug #165.
80 | """
81 | driver.get(base_url + '/languages')
82 | driver.find_element_by_link_text("C#").click()
83 | driver.assert_in_current_title("C#")
84 |
85 | # Get info to get the history page with requests.
86 | # We need the http code, which selenium won't give.
87 | button = driver.find_element_by_link_text("Show language history")
88 | history_page_url = button.get_attribute('href')
89 | resp = requests.get(history_page_url)
90 | assert resp.status_code == 200
91 |
92 |
93 | def test_history(driver, base_url):
94 | pass # TODO functional test of history page
95 |
--------------------------------------------------------------------------------
/functional_tests/stateless_tests/test_badge.py:
--------------------------------------------------------------------------------
1 | """
2 | Make sure badges are served.
3 | """
4 |
5 | import requests
6 |
7 |
8 | def test_badge(base_url):
9 | response = requests.get(base_url + '/badge/Nagasaki45/krihelinator')
10 | assert response.status_code == 200
11 |
12 |
13 | def test_non_existing_github_repo(base_url):
14 | response = requests.get(base_url + '/badge/no-such/repo')
15 | assert response.status_code == 404
16 |
--------------------------------------------------------------------------------
/functional_tests/stateless_tests/test_error_pages.py:
--------------------------------------------------------------------------------
1 | """
2 | Test pages that should lead to errors.
3 | """
4 |
5 | import requests
6 |
7 |
8 | def test_repositories_without_arguments(base_url):
9 | """
10 | Get /repositories without mentioning a repo name. Bug #140.
11 | """
12 | response = requests.get(base_url + '/repositories')
13 | assert response.status_code == 404
14 |
15 |
16 | def test_repos_of_unexisting_language(base_url):
17 | """
18 | Get repos of unexisting language. Bug #79.
19 | """
20 | response = requests.get(base_url + '/languages/moshe')
21 | assert response.status_code == 404
22 |
23 |
24 | def test_search_github_without_giving_a_repo_name(base_url):
25 | """
26 | Bug #145.
27 | """
28 | resp = requests.get(base_url + '/?query=&type=github')
29 | assert resp.status_code == 200
30 |
--------------------------------------------------------------------------------
/graphics/badge_demo.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagasaki45/krihelinator/243bfe476b8128dc2f0fcd913bebd8cf20b7deb6/graphics/badge_demo.xcf
--------------------------------------------------------------------------------
/graphics/logo_bright.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagasaki45/krihelinator/243bfe476b8128dc2f0fcd913bebd8cf20b7deb6/graphics/logo_bright.xcf
--------------------------------------------------------------------------------
/graphics/logo_dark.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagasaki45/krihelinator/243bfe476b8128dc2f0fcd913bebd8cf20b7deb6/graphics/logo_dark.xcf
--------------------------------------------------------------------------------
/graphics/logo_navbar.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagasaki45/krihelinator/243bfe476b8128dc2f0fcd913bebd8cf20b7deb6/graphics/logo_navbar.xcf
--------------------------------------------------------------------------------
/www/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM elixir:1.5.2
2 |
3 | RUN apt-get update && apt-get install -y inotify-tools
4 |
5 | WORKDIR /home/elixir/app
6 |
7 | RUN mix local.hex --force && mix local.rebar --force
8 |
9 | ENV MIX_ENV=prod
10 | COPY mix.exs ./
11 | COPY mix.lock ./
12 | RUN mix do deps.get, deps.compile
13 |
14 | COPY . ./
15 |
16 | CMD ["mix", "phoenix.server"]
17 |
--------------------------------------------------------------------------------
/www/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # General application configuration
9 | config :krihelinator,
10 | ecto_repos: [Krihelinator.Repo]
11 |
12 | # Configure background jobs
13 | config :quantum,
14 | default_overlap: false
15 |
16 | config :quantum, :krihelinator,
17 | cron: [
18 | periodic: [
19 | schedule: "0 */6 * * * *", # Every 6 hours
20 | task: {Krihelinator.Periodic, :run}
21 | ],
22 | keep_languages_history: [
23 | schedule: "0 5 */3 * * *", # Every 3 days on 5am
24 | task: {Krihelinator.History, :keep_languages_history}
25 | ]
26 | ]
27 |
28 | # Configures the endpoint
29 | config :krihelinator, Krihelinator.Web.Endpoint,
30 | http: [compress: true, port: 4000],
31 | secret_key_base: "WCLRK3yAFCHMKEC5+0WtAjzkm1vaRmzk0duH19wW9xC/l3Tb5eLdI0RYl/R7xCTR",
32 | render_errors: [view: Krihelinator.ErrorView, accepts: ~w(html json)],
33 | pubsub: [name: Krihelinator.PubSub,
34 | adapter: Phoenix.PubSub.PG2]
35 |
36 | # Configures Elixir's Logger
37 | config :logger, :console,
38 | format: "$metadata[$level] $message\n",
39 | metadata: [:request_id]
40 |
41 | # Configure your database
42 | config :krihelinator, Krihelinator.Repo,
43 | adapter: Ecto.Adapters.Postgres,
44 | username: "postgres",
45 | password: "postgres",
46 | database: "krihelinator_prod",
47 | hostname: "db",
48 | pool_size: 10,
49 | ownership_timeout: 60_000
50 |
51 | config :big_query,
52 | bigquery_private_key_path: "/secrets/bigquery_private_key.json"
53 |
54 | # Import environment specific config. This must remain at the bottom
55 | # of this file so it overrides the configuration defined above.
56 | import_config "#{Mix.env}.exs"
57 |
--------------------------------------------------------------------------------
/www/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 :krihelinator, Krihelinator.Web.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | # Watch static and templates for browser reloading.
15 | live_reload: [
16 | patterns: [
17 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
18 | ~r{lib/krihelinator/web/views/.*(ex)$},
19 | ~r{lib/krihelinator/web/templates/.*(eex)$}
20 | ]
21 | ]
22 |
23 |
24 | # Do not include metadata nor timestamps in development logs
25 | config :logger, :console, format: "[$level] $message\n"
26 |
27 | # Set a higher stacktrace during development. Avoid configuring such
28 | # in production as building large stacktraces may be expensive.
29 | config :phoenix, :stacktrace_depth, 20
30 |
31 |
--------------------------------------------------------------------------------
/www/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we configure the host to read the PORT
4 | # from the system environment. Therefore, you will need
5 | # to set PORT=80 before running your server.
6 | #
7 | # You should also configure the url host to something
8 | # meaningful, we use this information when generating URLs.
9 | #
10 | # Finally, we also include the path to a manifest
11 | # containing the digested version of static files. This
12 | # manifest is generated by the mix phoenix.digest task
13 | # which you typically run after static files are built.
14 | config :krihelinator, Krihelinator.Web.Endpoint,
15 | force_ssl: [hsts: true, host: nil],
16 | https: [
17 | port: 4040,
18 | compress: true,
19 | keyfile: "/etc/letsencrypt/live/krihelinator.xyz/privkey.pem",
20 | cacertfile: "/etc/letsencrypt/live/krihelinator.xyz/chain.pem",
21 | certfile: "/etc/letsencrypt/live/krihelinator.xyz/cert.pem"
22 | ],
23 | root: ".",
24 | server: true
25 |
26 | # Do not print debug messages in production
27 | config :logger, level: :info
28 |
29 | # Configure your database
30 | config :krihelinator, Krihelinator.Repo,
31 | pool_size: 20
32 |
33 | # ## SSL Support
34 | #
35 | # To get SSL working, you will need to add the `https` key
36 | # to the previous section and set your `:url` port to 443:
37 | #
38 | # config :krihelinator, Krihelinator.Web.Endpoint,
39 | # ...
40 | # url: [host: "example.com", port: 443],
41 | # https: [port: 443,
42 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
43 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
44 | #
45 | # Where those two env variables return an absolute path to
46 | # the key and cert in disk or a relative path inside priv,
47 | # for example "priv/ssl/server.key".
48 | #
49 | # We also recommend setting `force_ssl`, ensuring no data is
50 | # ever sent via http, always redirecting to https:
51 | #
52 | # config :krihelinator, Krihelinator.Web.Endpoint,
53 | # force_ssl: [hsts: true]
54 | #
55 | # Check `Plug.SSL` for all available options in `force_ssl`.
56 |
--------------------------------------------------------------------------------
/www/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 :krihelinator, Krihelinator.Web.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
12 | # Configure your database
13 | config :krihelinator, Krihelinator.Repo,
14 | database: "krihelinator_test",
15 | pool: Ecto.Adapters.SQL.Sandbox
16 |
--------------------------------------------------------------------------------
/www/lib/krihelinator.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator do
2 | use Application
3 | require Logger
4 |
5 | @moduledoc """
6 | The Krihelinator OTP application. Everything starts from here.
7 | """
8 |
9 | # See http://elixir-lang.org/docs/stable/elixir/Application.html
10 | # for more information on OTP Applications
11 | def start(_type, _args) do
12 | import Supervisor.Spec
13 |
14 | # Define workers and child supervisors to be supervised
15 | children = [
16 | # Start the Ecto repository
17 | supervisor(Krihelinator.Repo, []),
18 | # Start the endpoint when the application starts
19 | supervisor(Krihelinator.Web.Endpoint, []),
20 | ]
21 |
22 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
23 | # for other strategies and supported options
24 | opts = [strategy: :one_for_one, name: Krihelinator.Supervisor]
25 | Supervisor.start_link(children, opts)
26 | end
27 |
28 | # Tell Phoenix to update the endpoint configuration
29 | # whenever the application is updated.
30 | def config_change(changed, _new, removed) do
31 | Krihelinator.Web.Endpoint.config_change(changed, removed)
32 | :ok
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/github.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Github do
2 | @moduledoc """
3 | A context for Github models.
4 | """
5 |
6 | require Logger
7 | import Ecto.Query, only: [from: 2]
8 | alias Krihelinator.Github, as: GH
9 | alias Krihelinator.Repo
10 |
11 | @doc """
12 | Case-insensitive / scrapy get repo by name.
13 | """
14 | def get_repo_by_name(name) do
15 | query = from(r in GH.Repo,
16 | where: ilike(r.name, ^name))
17 | with {:ok, :nil} <- {:ok, Repo.one(query)},
18 | {:ok, data} <- Krihelinator.Scraper.scrape(name)
19 | do
20 | data = Map.put(data, :user_requested, true)
21 | %GH.Repo{}
22 | |> GH.Repo.changeset(data)
23 | |> Repo.insert()
24 | |> log_new_user_requested_repo()
25 | end
26 | end
27 |
28 | defp log_new_user_requested_repo({:ok, repo}) do
29 | Logger.info("New user_requested repo: #{repo.name}")
30 | {:ok, repo}
31 | end
32 | defp log_new_user_requested_repo(otherwise) do
33 | otherwise
34 | end
35 |
36 | @doc """
37 | Get all repos that partially match the given name.
38 | """
39 | def query_repos_by_name(query_string) do
40 | query = from(r in GH.Repo,
41 | where: ilike(r.name, ^"%#{query_string}%"),
42 | order_by: [desc: r.krihelimeter],
43 | limit: 50,
44 | preload: :language)
45 | Repo.all(query)
46 | end
47 |
48 | @doc """
49 | Straight forward get by name (!) with preloaded repos.
50 | """
51 | def get_language_by_name!(name) do
52 | repos_query = from(r in GH.Repo,
53 | order_by: [desc: r.krihelimeter],
54 | limit: 50)
55 |
56 | GH.Language
57 | |> Repo.get_by!(name: name)
58 | |> Repo.preload([repos: repos_query])
59 | end
60 |
61 | @doc """
62 | Straight forward all languages with krihelimeter > 0, in descending order.
63 | """
64 | def all_languages() do
65 | query = from(l in GH.Language,
66 | order_by: [{:desc, :krihelimeter}],
67 | where: l.krihelimeter > 0)
68 | Repo.all(query)
69 | end
70 |
71 | @doc """
72 | Local path to repo.
73 | """
74 | def repo_path(conn, full_name) do
75 | [user, repo] = String.split(full_name, "/")
76 | Krihelinator.Web.Router.Helpers.page_path(conn, :repository, user, repo)
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/github/language.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Github.Language do
2 | use Krihelinator.Web, :model
3 |
4 | @moduledoc """
5 | A programming language found on Github.
6 | """
7 |
8 | @derive {Poison.Encoder, only: ~w(id name krihelimeter)a}
9 | schema "languages" do
10 | field :name, :string
11 | field :krihelimeter, :integer
12 | has_many :repos, Krihelinator.Github.Repo
13 | has_many :history, Krihelinator.History.Language
14 |
15 | timestamps()
16 | end
17 |
18 | @doc """
19 | Builds a changeset based on the `struct` and `params`.
20 | """
21 | def changeset(struct, params \\ %{}) do
22 | struct
23 | |> cast(params, [:name, :krihelimeter])
24 | |> validate_required([:name])
25 | |> unique_constraint(:name)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/github/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Github.Repo do
2 | use Krihelinator.Web, :model
3 | alias Krihelinator.Github, as: GH
4 |
5 | @moduledoc """
6 | Ecto model of a repository on github.
7 | """
8 |
9 | @derive {
10 | Poison.Encoder, only:
11 | ~w(id name description language_id merged_pull_requests
12 | proposed_pull_requests closed_issues new_issues commits authors
13 | trending user_requested)a
14 | }
15 | schema "repos" do
16 | field :name, :string
17 | field :description, :string
18 | field :language_name, :string, virtual: true
19 | field :fork_of, :string, virtual: true
20 | belongs_to :language, GH.Language
21 | field :merged_pull_requests, :integer
22 | field :proposed_pull_requests, :integer
23 | field :closed_issues, :integer
24 | field :new_issues, :integer
25 | field :commits, :integer
26 | field :authors, :integer
27 | field :forks, :integer
28 | field :krihelimeter, :integer
29 | field :trending, :boolean, default: false
30 | field :user_requested, :boolean, default: false
31 | field :dirty, :boolean, default: false
32 |
33 | timestamps()
34 | end
35 |
36 | @allowed ~w(name description language_name fork_of merged_pull_requests
37 | proposed_pull_requests closed_issues new_issues commits authors
38 | forks trending user_requested dirty)a
39 | @required ~w(name merged_pull_requests proposed_pull_requests
40 | closed_issues new_issues commits authors)a
41 |
42 | @doc """
43 | Builds a changeset based on the `struct` and `params`.
44 | """
45 | def changeset(struct, params \\ %{}) do
46 | struct
47 | |> cast(params, @allowed)
48 | |> validate_required(@required)
49 | |> validate_number(:merged_pull_requests, greater_than_or_equal_to: 0)
50 | |> validate_number(:proposed_pull_requests, greater_than_or_equal_to: 0)
51 | |> validate_number(:closed_issues, greater_than_or_equal_to: 0)
52 | |> validate_number(:new_issues, greater_than_or_equal_to: 0)
53 | |> validate_number(:commits, greater_than_or_equal_to: 0)
54 | |> validate_number(:authors, greater_than_or_equal_to: 0)
55 | |> validate_number(:forks, greater_than_or_equal_to: 0)
56 | |> apply_restrictive_validations()
57 | |> unique_constraint(:name)
58 | |> set_krihelimeter
59 | |> put_language_assoc()
60 | |> clear_dirty_bit()
61 | end
62 |
63 | @doc """
64 | If not user_requested make sure stats are above thresholds.
65 | """
66 | def apply_restrictive_validations(changeset) do
67 | if Ecto.Changeset.get_field(changeset, :user_requested) do
68 | changeset
69 | else
70 | changeset
71 | |> Ecto.Changeset.validate_number(:forks, greater_than_or_equal_to: 10)
72 | |> Ecto.Changeset.validate_number(:krihelimeter, greater_than_or_equal_to: 30)
73 | |> Ecto.Changeset.validate_number(:authors, greater_than_or_equal_to: 2)
74 | |> Ecto.Changeset.validate_inclusion(:fork_of, [nil])
75 | end
76 | end
77 |
78 | @krihelimeter_coefficients [
79 | merged_pull_requests: 8,
80 | proposed_pull_requests: 8,
81 | closed_issues: 8,
82 | new_issues: 8,
83 | commits: 1,
84 | authors: 20,
85 | ]
86 |
87 | @doc """
88 | Use the existing data, and the expected changes, to calculate and set the
89 | krihelimeter.
90 | """
91 | def set_krihelimeter(%{valid?: false} = changeset) do
92 | changeset
93 | end
94 | def set_krihelimeter(changeset) do
95 | krihelimeter = Enum.sum(
96 | for {field, coefficient} <- @krihelimeter_coefficients do
97 | {_change_or_data, value} = fetch_field(changeset, field)
98 | value * coefficient
99 | end
100 | )
101 | put_change(changeset, :krihelimeter, krihelimeter)
102 | end
103 |
104 | def put_language_assoc(%{changes: %{language_name: lang}} = changeset) do
105 | {:ok, language} = Krihelinator.Repo.get_or_create_by(GH.Language, name: lang)
106 | changeset
107 | |> put_change(:language_id, language.id)
108 | end
109 | def put_language_assoc(changeset) do
110 | changeset
111 | end
112 |
113 | def clear_dirty_bit(changeset) do
114 | put_change(changeset, :dirty, false)
115 | end
116 |
117 | end
118 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/history.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.History do
2 | require Logger
3 |
4 | @moduledoc """
5 | A module to keep track of the history.
6 | """
7 |
8 | import Ecto.Query, only: [from: 2]
9 |
10 | @doc """
11 | Create new History.Language record with stats for any language in the DB.
12 | """
13 | def keep_languages_history() do
14 | Logger.info "History.keep_languages_history() kicked in!"
15 | Krihelinator.Github.Language
16 | |> Krihelinator.Repo.all()
17 | |> Enum.each(&keep_language_history/1)
18 | Logger.info "History.keep_languages_history() finished successfully!"
19 | end
20 |
21 | defp keep_language_history(language) do
22 | params = %{krihelimeter: language.krihelimeter,
23 | timestamp: DateTime.utc_now()}
24 | %Krihelinator.History.Language{}
25 | |> Krihelinator.History.Language.changeset(params)
26 | |> Ecto.Changeset.put_assoc(:language, language)
27 | |> Krihelinator.Repo.insert!()
28 | end
29 |
30 | def get_languages_history_json(language_names) do
31 | query = from(h in Krihelinator.History.Language,
32 | join: l in assoc(h, :language),
33 | where: l.name in ^language_names,
34 | order_by: :timestamp,
35 | preload: :language)
36 |
37 | query
38 | |> Krihelinator.Repo.all()
39 | |> Stream.map(&(
40 | %{name: &1.language.name,
41 | timestamp: &1.timestamp,
42 | krihelimeter: &1.krihelimeter}
43 | ))
44 | |> Poison.encode!()
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/history/language.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.History.Language do
2 | use Krihelinator.Web, :model
3 |
4 | @moduledoc """
5 | Keep a point in time for the language statistics. Used for visualizing
6 | language trends.
7 | """
8 |
9 | @derive {Poison.Encoder, only: ~w(id language_id krihelimeter timestamp)a}
10 | schema "languages_history" do
11 | field :name, :string
12 | belongs_to :language, Krihelinator.Github.Language
13 | field :krihelimeter, :integer
14 | field :timestamp, :utc_datetime
15 | end
16 |
17 | @allowed_and_required ~w(krihelimeter timestamp)a
18 |
19 | @doc """
20 | Builds a changeset based on the `struct` and `params`.
21 | """
22 | def changeset(struct, params \\ %{}) do
23 | struct
24 | |> cast(params, @allowed_and_required)
25 | |> validate_required(@allowed_and_required)
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/import_export.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.ImportExport do
2 | @moduledoc """
3 | Tools to export all of the data from the DB into a single json, and
4 | to populate the DB from an existing one.
5 | """
6 |
7 | alias Krihelinator.Github, as: GH
8 | @models [GH.Language, GH.Repo, Krihelinator.History.Language]
9 |
10 | @doc """
11 | Populate the DB with data from json string.
12 |
13 | json: A string.
14 | repo: An Ecto repo.
15 | """
16 | def import_data(data, repo) do
17 | decoded = decode_data(data)
18 | do_import_data(decoded, repo)
19 | Enum.each(decoded, &fix_postgres_next_val(&1.model, repo))
20 | end
21 |
22 | defp decode_data(data) do
23 | model_strings = Enum.map(@models, &Atom.to_string/1)
24 | data
25 | |> Poison.decode!(keys: :atoms!)
26 | |> Enum.filter(fn model_data -> model_data.model in model_strings end)
27 | |> Enum.map(fn model_data ->
28 | %{model_data | model: String.to_existing_atom(model_data.model)}
29 | end)
30 | end
31 |
32 | defp do_import_data(data, repo) do
33 | data
34 | |> Stream.flat_map(fn %{model: model, items: items} ->
35 | Stream.map(items, &create_changeset(model, &1))
36 | end)
37 | |> Enum.each(&repo.insert!/1)
38 | end
39 |
40 | # Fix for #163, reset postgres next_val
41 | defp fix_postgres_next_val(model, repo) do
42 | table = model.__schema__(:source)
43 | sql = "SELECT setval('#{table}_id_seq', (SELECT MAX(id) from \"#{table}\"));"
44 | Ecto.Adapters.SQL.query(repo, sql, [])
45 | end
46 |
47 |
48 | defp create_changeset(model, item) do
49 | associations = Enum.filter(item, fn {key, _} -> is_association_key?(key) end)
50 | {id, params} = Map.pop(item, :id)
51 |
52 | model
53 | |> struct(id: id)
54 | |> model.changeset(params)
55 | |> Ecto.Changeset.change(associations)
56 | end
57 |
58 | defp is_association_key?(key) do
59 | key
60 | |> Atom.to_string()
61 | |> String.ends_with?("_id")
62 | end
63 |
64 | @doc """
65 | A wrapper around the `export_data` function with the relevant models and
66 | repo.
67 | """
68 | def export_krihelinator_data() do
69 | export_data(@models, Krihelinator.Repo)
70 | end
71 |
72 | @doc """
73 | Create a json string of the data.
74 |
75 | models: A list of models (modules). The model is passed to `repo.all`.
76 | Models can derive from `Poison.Encoder` with the `:only` option to restrict
77 | serialized fields.
78 | repo: A module that have an `all` function that is called with the models to
79 | get all of the items for each model.
80 | """
81 | def export_data(models, repo) do
82 | models
83 | |> Stream.map(fn (model) ->
84 | %{model: Atom.to_string(model), items: repo.all(model)}
85 | end)
86 | |> Poison.encode!()
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/periodic.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Periodic do
2 | require Logger
3 | import Ecto.Query, only: [from: 2]
4 | alias Krihelinator.Repo
5 | alias Krihelinator.Github, as: GH
6 |
7 | @moduledoc """
8 | A background task that needs to run periodically on the Krihelinator server.
9 |
10 | - Mark all github repos as "dirty".
11 | - Scrape the github trending page for interesting, active, projects.
12 | - Using BigQuery, get and scrape all repos that at least 2 users pushed
13 | commits to.
14 | - Get the remaining "dirty" repos and pass through the scraper again, to
15 | update stats.
16 | - Clean the remaining dirty repos. These repos failed to update or fell bellow
17 | activity threshold.
18 | - Update the total krihelimeter for all languages.
19 | """
20 |
21 | defp periodically_gc(pid) do
22 | # This is an ugly hack to force GC every now and then on the periodic
23 | # process. TODO search for a better solution!
24 | Process.sleep(30_000)
25 | if Process.alive?(pid) do
26 | :erlang.garbage_collect(pid)
27 | periodically_gc(pid)
28 | end
29 | end
30 |
31 | @doc """
32 | Main entry point.
33 | """
34 | def run() do
35 | my_pid = self()
36 | Task.async(fn -> periodically_gc(my_pid) end)
37 | Logger.info "Periodic process kicked in!"
38 | set_dirty_bit()
39 | scrape_trending()
40 | scrape_from_bigquery()
41 | rescrape_still_dirty()
42 | clean_dirty()
43 | update_languages_stats()
44 | Logger.info "Periodic process finished successfully!"
45 | end
46 |
47 | def set_dirty_bit() do
48 | Logger.info "Setting dirty bit for all"
49 | Repo.update_all(GH.Repo, set: [dirty: true])
50 | end
51 |
52 | @doc """
53 | Scrape the github trending page and update repos.
54 | """
55 | def scrape_trending() do
56 | Logger.info "Resetting trendiness for all"
57 | Repo.update_all(GH.Repo, set: [trending: false])
58 | Logger.info "Scraping trending"
59 | names = Krihelinator.Periodic.GithubTrending.scrape()
60 | async_handle(names, trending: true)
61 | end
62 |
63 | @doc """
64 | Request repos from google BigQuery, scrape, and persist.
65 | """
66 | def scrape_from_bigquery() do
67 | Logger.info "Getting repositories from BigQuery to scrape"
68 | names = Krihelinator.Periodic.BigQuery.query()
69 | async_handle(names)
70 | end
71 |
72 | @doc """
73 | Get the rest of the repositories that weren't updated from github trending
74 | or BigQuery and rescrape.
75 | """
76 | def rescrape_still_dirty() do
77 | query = from(r in GH.Repo, where: r.dirty, select: r.name)
78 | names = Repo.all(query)
79 | Logger.info "Rescraping #{length(names)} still dirty repositories"
80 | async_handle(names)
81 | end
82 |
83 | @async_params [max_concurrency: 20, on_timeout: :kill_task]
84 |
85 | @doc """
86 | Scrape and persist repositories concurrently with `Task.async_stream`.
87 | """
88 | def async_handle(names, extra_params \\ []) do
89 | names
90 | |> Task.async_stream(&handle(&1, extra_params), @async_params)
91 | |> Enum.reduce(%{}, fn x, acc -> Map.update(acc, x, 1, &(&1 + 1)) end)
92 | |> Enum.each(&log_aggregated_results/1)
93 | end
94 |
95 | @doc """
96 | Scrape and persist a single repository by name.
97 | """
98 | def handle(name, extra_params) do
99 | name
100 | |> scrape_with_retries()
101 | |> put_extra_params(extra_params)
102 | |> persist()
103 | |> simplify_result()
104 | end
105 |
106 | @no_retry ~w(page_not_found dmca_takedown)a
107 | @max_attempts 3
108 |
109 | def scrape_with_retries(name, attempt \\ 1) do
110 | case Krihelinator.Scraper.scrape(name) do
111 | {:error, error} when error not in @no_retry ->
112 | if attempt <= @max_attempts do
113 | scrape_with_retries(name, attempt + 1)
114 | else
115 | Logger.error "Scraping #{name} failed with #{inspect(error)}"
116 | {:error, error}
117 | end
118 | otherwise ->
119 | otherwise
120 | end
121 | end
122 |
123 | def put_extra_params({:error, error}, _extra_params) do
124 | {:error, error}
125 | end
126 | def put_extra_params({:ok, data}, extra_params) do
127 | {:ok, Map.merge(data, Map.new(extra_params))}
128 | end
129 |
130 | def persist({:error, error}) do
131 | {:error, error}
132 | end
133 | def persist({:ok, data}) do
134 | Repo.update_or_create_from_data(GH.Repo, data, by: :name)
135 | end
136 |
137 | def simplify_result({:error, %Ecto.Changeset{}}), do: :validation_error
138 | def simplify_result({:error, error}), do: error
139 | def simplify_result({:ok, _whatever}), do: :ok
140 |
141 | def log_aggregated_results({key, count}) do
142 | Logger.info "#{count} operations ended with #{inspect(key)}"
143 | end
144 |
145 | @doc """
146 | Clean the DB from repositories that failed to update properly in the last
147 | periodic loop.
148 | """
149 | def clean_dirty do
150 | Logger.info "Cleaning dirty repos"
151 | query = from(r in GH.Repo, where: r.dirty)
152 | {num, _whatever} = Repo.delete_all(query)
153 | Logger.info "Cleaned #{num} dirty repos"
154 | end
155 |
156 | @doc """
157 | Update the total krihelimeter for all languages.
158 | """
159 | def update_languages_stats() do
160 | Logger.info "Updating languages statistics"
161 | GH.Language
162 | |> Repo.all()
163 | |> Repo.preload(:repos)
164 | |> Enum.each(fn language ->
165 | changes = %{
166 | krihelimeter: Enum.sum(for r <- language.repos, do: r.krihelimeter),
167 | }
168 | language
169 | |> GH.Language.changeset(changes)
170 | |> Repo.update()
171 | end)
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/periodic/big_query.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Periodic.BigQuery do
2 | require Logger
3 |
4 | @moduledoc """
5 | Issue pre-configured queries against google BigQuery and (partly) process
6 | the results.
7 | """
8 |
9 | def query() do
10 | {start_date, end_date} = get_start_and_end_dates()
11 | {:ok, %{jobComplete: true, rows: rows}} = run_query(start_date, end_date)
12 | Logger.info "Got #{length(rows)} active repos between #{start_date} and #{end_date} from BigQuery"
13 | process_rows(rows)
14 | end
15 |
16 | defp get_start_and_end_dates() do
17 | end_date = Date.utc_today()
18 | start_date = Timex.shift(end_date, days: -7)
19 | {Date.to_string(start_date), Date.to_string(end_date)}
20 | end
21 |
22 | defp run_query(start_date, end_date) do
23 | query_string = "SELECT name, COUNT(DISTINCT author) AS authors FROM (SELECT type, repo.name AS name, actor.id AS author, FROM TABLE_DATE_RANGE([githubarchive:day.], TIMESTAMP('#{start_date}'), TIMESTAMP('#{end_date}')) WHERE type = 'PushEvent') GROUP BY name HAVING authors >= 2 ORDER BY authors DESC;"
24 | query_struct = %BigQuery.Types.Query{query: query_string}
25 | BigQuery.Job.query("krihelinator", query_struct)
26 | end
27 |
28 | defp process_rows(rows) do
29 | rows
30 | |> Stream.map(fn datum -> Map.fetch!(datum, :f) end)
31 | |> Stream.map(&hd/1)
32 | |> Enum.map(fn datum -> Map.fetch!(datum, :v) end)
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/periodic/github_trending.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Periodic.GithubTrending do
2 |
3 | @moduledoc """
4 | Helper module for scrape and parsing the github trending page.
5 | """
6 |
7 | @doc """
8 | Scrape the github trending page and return stream of repos to scrape.
9 | """
10 | def scrape do
11 | %{body: body, status_code: 200} = HTTPoison.get!("https://github.com/trending")
12 | parse(body)
13 | end
14 |
15 | @doc """
16 | Parse the github trending page. Returns a list of maps with name and
17 | description.
18 | """
19 | def parse(html) do
20 | html
21 | |> Floki.find(".repo-list li")
22 | |> Enum.map(&parse_name/1)
23 | end
24 |
25 | @doc """
26 | Parse the repo name (user/repo) from the repo floki item.
27 | """
28 | def parse_name(floki_item) do
29 | floki_item
30 | |> Floki.find("h3 a")
31 | |> Floki.attribute("href")
32 | |> hd
33 | |> String.trim_leading("/")
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo do
2 | use Ecto.Repo, otp_app: :krihelinator
3 |
4 | @doc """
5 | Given a model and an keyword list get or create a row in the DB. Return
6 | signature is the same as Repo.insert.
7 | Note that the the keyword list passes through the model changeset.
8 | """
9 | def get_or_create_by(model, keywords) do
10 | case get_by(model, keywords) do
11 | nil ->
12 | model
13 | |> struct
14 | |> model.changeset(Enum.into(keywords, %{}))
15 | |> insert
16 | struct -> {:ok, struct}
17 | end
18 | end
19 |
20 | @doc """
21 | Update existing struct or create new one using data map. Return signature
22 | is the same as Repo.insert_or_update.
23 | Note that the the map passes through the model changeset.
24 | """
25 | def update_or_create_from_data(model, data, by: by) do
26 | model
27 | |> get_by([{by, Map.fetch!(data, by)}])
28 | |> case do
29 | :nil -> struct(model)
30 | struct -> struct
31 | end
32 | |> model.changeset(data)
33 | |> insert_or_update()
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/scraper.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Scraper do
2 |
3 | @moduledoc """
4 | General github scraping logic. Usefull for repo page scraping and pulse page
5 | scraping.
6 | """
7 |
8 | require Logger
9 |
10 | @doc """
11 | Scrape repo home and pulse pages.
12 | """
13 | def scrape(name) do
14 | with {:ok, map} <- scrape_repo_page(%{name: name}),
15 | {:ok, map} <- scrape_pulse_page(map)
16 | do
17 | {:ok, %{map | name: "#{map.user_name}/#{map.repo_name}"}}
18 | end
19 | end
20 |
21 | @basic_elements [
22 | {:user_name, ~s{span[itemprop="author"]}, :string},
23 | {:repo_name, ~s{strong[itemprop="name"]}, :string},
24 | {:fork_of, ~s{span[class="fork-flag"]}, :string},
25 | {:description, ~s{span[itemprop="about"]}, :string},
26 | {:language_name, ~s{span[class="lang"]}, :string},
27 | ]
28 |
29 | @pulse_elements [
30 | {:merged_pull_requests, ~s{a[href="#merged-pull-requests"]}, ~r/(?\d+) Merged Pull Requests/},
31 | {:proposed_pull_requests, ~s{a[href="#proposed-pull-requests"]}, ~r/(?\d+) Proposed Pull Requests/},
32 | {:closed_issues, ~s{a[href="#closed-issues"]}, ~r/(?\d+) Closed Issues/},
33 | {:new_issues, ~s{a[href="#new-issues"]}, ~r/(?\d+) New Issues/},
34 | {:commits, "div.section.diffstat-summary", ~r/pushed (?\d+) commits to/},
35 | {:authors, "div.section.diffstat-summary", ~r/(?\d+) author/},
36 | {:forks, "ul.pagehead-actions", ~r/Fork (?\d+)/},
37 | ]
38 |
39 | @doc """
40 | Scrape statistics about a repository from it's homepage.
41 | """
42 | def scrape_repo_page(map) do
43 | scrape(map, "", @basic_elements)
44 | end
45 |
46 | @doc """
47 | Scrape statistics about a repository from github's pulse page.
48 | """
49 | def scrape_pulse_page(map) do
50 | scrape(map, "/pulse", @pulse_elements)
51 | end
52 |
53 | @doc """
54 | Common scraping function.
55 | """
56 | def scrape(map, suffix, elements) do
57 | with {:ok, resp} <- http_get("https://github.com/#{map.name}#{suffix}"),
58 | {:ok, new_data} <- handle_response(resp, elements)
59 | do
60 | {:ok, Map.merge(map, new_data)}
61 | end
62 | end
63 |
64 | @doc """
65 | A wrapper around `HTTPoison.get` with extra options.
66 | """
67 | def http_get(url) do
68 | headers = []
69 | options = [recv_timeout: 10_000, follow_redirect: true]
70 | case HTTPoison.get(url, headers, options) do
71 | {:ok, resp} -> {:ok, resp}
72 | {:error, error} -> {:error, error.reason}
73 | end
74 | end
75 |
76 | @doc """
77 | Analyze the HTTPoison response, returns a map to update the repo with.
78 | Several errors are ignorable, collect them, the callers will have to decide
79 | what to do with them.
80 | """
81 | def handle_response(%{status_code: 200, body: body}, elements) do
82 | {:ok, parse(body, elements)}
83 | end
84 | def handle_response(%{status_code: 404}, _elements), do: {:error, :page_not_found}
85 | def handle_response(%{status_code: 451}, _elements), do: {:error, :dmca_takedown}
86 | def handle_response(%{status_code: 500}, _elements), do: {:error, :github_server_error}
87 | def handle_response(%{status_code: code}, _elements) do
88 | Logger.error "Unknown scraping error occurred (#{code})."
89 | {:error, :unknown_scraping_error}
90 | end
91 |
92 | @doc """
93 | Use [floki](https://github.com/philss/floki) to parse the page and return
94 | a map for that repo.
95 | """
96 | def parse(body, elements) do
97 | floki = Floki.parse(body)
98 | for {key, css_selector, regex_pattern} <- elements, into: %{} do
99 | {key, general_extractor(floki, css_selector, regex_pattern)}
100 | end
101 | end
102 |
103 | @doc """
104 | Extracts information from the "floki-parsed" html using css selectors and
105 | regex matching on the resulting text.
106 | """
107 | def general_extractor(floki, css_selector, :string) do
108 | case basic_text_extraction(floki, css_selector) do
109 | "" -> :nil
110 | string -> string
111 | end
112 | end
113 | def general_extractor(floki, css_selector, regex_pattern) do
114 | floki
115 | |> basic_text_extraction(css_selector)
116 | |> String.replace(",", "") # Numbers are comma separated
117 | |> (&Regex.named_captures(regex_pattern, &1)).()
118 | |> case do
119 | %{"value" => value} -> String.to_integer(value)
120 | _ -> 0
121 | end
122 | end
123 |
124 | def basic_text_extraction(floki, css_selector) do
125 | case Floki.find(floki, css_selector) do
126 | [] -> ""
127 | otherwise ->
128 | otherwise
129 | |> hd
130 | |> Floki.text
131 | |> to_one_line
132 | end
133 | end
134 |
135 | @doc """
136 | Remove new lines and extra spaces from strings.
137 |
138 | Example:
139 |
140 | iex> import Krihelinator.Scraper, only: [to_one_line: 1]
141 | iex> to_one_line("hello\\nworld")
142 | "hello world"
143 | iex> to_one_line(" too\\n many spaces ")
144 | "too many spaces"
145 | """
146 | def to_one_line(text) do
147 | text
148 | |> String.split
149 | |> Enum.join(" ")
150 | end
151 |
152 | end
153 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.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 Krihelinator.Web, :controller
9 | use Krihelinator.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 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query
26 | end
27 | end
28 |
29 | def controller do
30 | quote do
31 | use Phoenix.Controller
32 |
33 | alias Krihelinator.Repo
34 | alias Krihelinator.Github, as: GH
35 | import Ecto
36 | import Ecto.Query
37 |
38 | import Krihelinator.Web.Router.Helpers
39 | end
40 | end
41 |
42 | def view do
43 | quote do
44 | use Phoenix.View, root: "lib/krihelinator/web/templates"
45 |
46 | # Import convenience functions from controllers
47 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1, action_name: 1]
48 |
49 | # Use all HTML functionality (forms, tags, etc)
50 | use Phoenix.HTML
51 |
52 | import Krihelinator.Web.Router.Helpers
53 | end
54 | end
55 |
56 | def router do
57 | quote do
58 | use Phoenix.Router
59 | end
60 | end
61 |
62 | @doc """
63 | When used, dispatch to the appropriate controller/view/etc.
64 | """
65 | defmacro __using__(which) when is_atom(which) do
66 | apply(__MODULE__, which, [])
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/controllers/badge_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.BadgeController do
2 | use Krihelinator.Web, :controller
3 |
4 | def badge(conn, %{"user" => user, "repo" => repo}) do
5 | case GH.get_repo_by_name("#{user}/#{repo}") do
6 | {:error, _whatever} ->
7 | conn
8 | |> put_status(:not_found)
9 | |> render("error.json")
10 | {:ok, model} ->
11 | render conn, "badge.svg", repo: model
12 | end
13 | end
14 |
15 | end
16 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/controllers/data_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.DataController do
2 | use Krihelinator.Web, :controller
3 |
4 | def all(conn, _params) do
5 | data = Krihelinator.ImportExport.export_krihelinator_data()
6 |
7 | now = DateTime.utc_now()
8 | format = "dump_%Y-%m-%d_%H_%M_%S.json"
9 | {:ok, filename} = Timex.format(now, format, :strftime)
10 |
11 | conn
12 | |> put_resp_content_type("text/json")
13 | |> put_resp_header("content-disposition", "attachment; filename=\"#{filename}\"")
14 | |> send_resp(200, data)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.PageController do
2 | use Krihelinator.Web, :controller
3 |
4 | def repositories(conn, %{"query" => ""}) do
5 | conn
6 | |> put_flash(:error, "No search query was provided.")
7 | |> repositories(%{})
8 | end
9 | def repositories(conn, %{"query" => query_string, "type" => "github"}) do
10 | case GH.get_repo_by_name(query_string) do
11 | {:error, _whatever} ->
12 | conn
13 | |> put_flash(:error, "The repository \"#{query_string}\" does not exist.")
14 | |> repositories(%{})
15 | {:ok, model} ->
16 | redirect(conn, to: GH.repo_path(conn, model.name))
17 | end
18 | end
19 | def repositories(conn, params) do
20 | query_string = Map.get(params, "query")
21 | repos = GH.query_repos_by_name(query_string)
22 | render conn, "repositories.html", repos: repos
23 | end
24 |
25 | def repository(conn, %{"user" => user, "repo" => repo}) do
26 | repository_name = "#{user}/#{repo}"
27 | case GH.get_repo_by_name(repository_name) do
28 | {:error, _error} ->
29 | conn
30 | |> put_status(:not_found)
31 | |> render(Krihelinator.ErrorView, "404.html")
32 | {:ok, repo} ->
33 | repo = Repo.preload(repo, :language)
34 | render(conn, "repository.html", repo: repo)
35 | end
36 | end
37 |
38 | def language(conn, %{"language" => language_name}) do
39 | language = GH.get_language_by_name!(language_name)
40 | render(conn, "language.html", language: language)
41 | end
42 |
43 | def languages(conn, _params) do
44 | languages = GH.all_languages()
45 | render(conn, "languages.html", languages: languages)
46 | end
47 |
48 | def languages_history(conn, params) do
49 | case Krihelinator.InputValidator.validate_history_query(params) do
50 |
51 | {:ok, language_names} ->
52 | json = Krihelinator.History.get_languages_history_json(language_names)
53 | render conn, "languages_history.html", json: json
54 |
55 | {:error, error} ->
56 | conn
57 | |> put_flash(:error, error)
58 | |> put_status(:bad_request)
59 | |> render(Krihelinator.ErrorView, "400.html")
60 | end
61 |
62 | end
63 |
64 | def about(conn, _params) do
65 | render conn, "about.html"
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Web.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :krihelinator
3 |
4 | # Serve at "/" the static files from "priv/static" directory.
5 | #
6 | # You should set gzip to true if you are running phoenix.digest
7 | # when deploying your static files in production.
8 | plug Plug.Static,
9 | at: "/", from: :krihelinator, gzip: true,
10 | only: ~w(css fonts images js media favicon.png robots.txt
11 | loaderio-af9cda539c1b3a4a235147af21f0fe5d.txt)
12 |
13 | plug Plug.Static,
14 | at: "/", from: "/webroot", gzip: true
15 |
16 | # Code reloading can be explicitly enabled under the
17 | # :code_reloader configuration of your endpoint.
18 | if code_reloading? do
19 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
20 | plug Phoenix.LiveReloader
21 | plug Phoenix.CodeReloader
22 | end
23 |
24 | plug Plug.RequestId
25 | plug Plug.Logger
26 |
27 | plug Plug.Parsers,
28 | parsers: [:urlencoded, :multipart, :json],
29 | pass: ["*/*"],
30 | json_decoder: Poison
31 |
32 | plug Plug.MethodOverride
33 | plug Plug.Head
34 |
35 | # The session will be stored in the cookie and signed,
36 | # this means its contents can be read but not tampered with.
37 | # Set :encryption_salt if you would also like to encrypt it.
38 | plug Plug.Session,
39 | store: :cookie,
40 | key: "_krihelinator_key",
41 | signing_salt: "lZoUI4Gb"
42 |
43 | plug Krihelinator.Web.Router
44 | end
45 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/input_validator.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.InputValidator do
2 |
3 | @moduledoc """
4 | A general input validation module. Configurable functions to validate
5 | user input.
6 | """
7 |
8 | @doc """
9 | Validates a field against a list of valid values or using parsing function.
10 | """
11 | def verify_field(params, field, allowed, default) do
12 | case Map.get(params, field) do
13 | :nil -> {:ok, default}
14 | value ->
15 | if is_list(allowed) do
16 | verify_value(value, allowed)
17 | else
18 | allowed.(value)
19 | end
20 | end
21 | end
22 |
23 | defp verify_value(value, allowed) do
24 | if Enum.member?(allowed, value) do
25 | {:ok, String.to_existing_atom(value)}
26 | else
27 | {:error, "#{inspect(value)} is not a valid value"}
28 | end
29 | end
30 |
31 | ###### Krihelinator specific ######
32 |
33 | def verify_languages_list(languages) do
34 | if is_list(languages) do
35 | {:ok, languages}
36 | else
37 | {:error, "Languages are expected to be given in a list"}
38 | end
39 | end
40 |
41 | def nicer_poison_decode(json) do
42 | case Poison.decode(json) do
43 | {:error, _whatever} -> {:error, "Failed to decode json"}
44 | otherwise -> otherwise
45 | end
46 | end
47 |
48 | def validate_history_query(params) do
49 | with {:ok, languages} <-
50 | verify_field(params, "languages", &nicer_poison_decode/1, []),
51 | {:ok, languages} <-
52 | verify_languages_list(languages),
53 | do: {:ok, languages}
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Web.Router do
2 | use Krihelinator.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 | end
11 |
12 | scope "/", Krihelinator do
13 | pipe_through :browser # Use the default browser stack
14 |
15 | get "/", PageController, :repositories
16 | get "/repositories/:user/:repo", PageController, :repository
17 | get "/languages", PageController, :languages
18 | get "/languages/:language", PageController, :language
19 | get "/languages-history", PageController, :languages_history
20 | get "/about", PageController, :about
21 | end
22 |
23 | scope "/badge", Krihelinator do
24 | get "/:user/:repo", BadgeController, :badge
25 | end
26 |
27 | scope "/data", Krihelinator do
28 | get "/", DataController, :all
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/templates/badge/badge.svg.eex:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | <%= page_title(assigns) %>
12 |
13 |
14 |
15 | ">
16 |
17 |
18 |
19 |
4 |
5 | This project proposes an alternative to <%= link "github's trending page", to: "https://www.github.com/trending" %>, by exposing projects with high contribution rate, instead of daily stars (similarly to github's pulse page).
6 | The krihelimeter of each repository is calculated using the number of authors, commits, pull requests, and issues of that project, from the past week.
7 |
8 |
9 |
10 |
11 |
12 |
Krihelimeter =
13 |
20
14 |
* authors +
15 |
16 |
17 |
18 |
8
19 |
* merged and proposed pull requests +
20 |
21 |
22 |
23 |
8
24 |
* new and closed issues +
25 |
26 |
27 |
28 |
1
29 |
* commits
30 |
31 |
32 |
33 |
34 | During the development of this project I found out that people use github as a backup service, automating hundreds of commits per week.
35 | Therefor, to filter these projects out, only projects with more than one author enters the Krihelinator DB.
36 |
37 |
Want to know more?
38 |
39 | The Krihelinator is an <%= link "open source project", to: "https://www.github.com/Nagasaki45/krihelinator" %> written in <%= link "elixir", to: "http://elixir-lang.org/" %>.
40 | Contributions, of any kind, are more than welcome and highly appreciated :-).
41 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/templates/page/badge.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
Claim a Krihelimeter badge
3 |
Let GitHub know how active is your project by adding a Krihelimeter badge to your README.
4 |
5 |
6 |
7 |
8 |
9 | Note! URLs for badges are case sensitive.
10 |
10 | <%= link "GitHub", to: "http://github.com/#{@repo.name}", class: "label label-info badge-item"%>
11 | <%= if @repo.language do %>
12 | <%= link @repo.language.name, to: page_path(@conn, :language, @repo.language.name), class: "label label-default"%>
13 | <% end %>
14 | <%= if @repo.trending do %>
15 |
88 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/views/badge_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.BadgeView do
2 | use Krihelinator.Web, :view
3 |
4 | def render("error.json", _opts) do
5 | %{"error": "Failed to process badge"}
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.ErrorView do
2 | use Krihelinator.Web, :view
3 |
4 | def render("400.html", _assigns) do
5 | "Bad request"
6 | end
7 |
8 | def render("404.html", _assigns) do
9 | "Page not found"
10 | end
11 |
12 | def render("500.html", _assigns) do
13 | "Internal server error"
14 | end
15 |
16 | # In case no render clause matches or no
17 | # template is found, let's render it as 500
18 | def template_not_found(_template, assigns) do
19 | render "500.html", assigns
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.LayoutView do
2 | use Krihelinator.Web, :view
3 |
4 | def navbar_link(assigns) do
5 | render "navbar_link.html", assigns
6 | end
7 |
8 | def page_title(assigns) do
9 | case action_name(assigns.conn) do
10 | :languages -> "Languages | the Krihelinator"
11 | :about -> "About | the Krihelinator"
12 | :language -> "#{assigns.language.name} | the Krihelinator"
13 | :languages_history -> "Languages history | the Krihelinator"
14 | _whatever -> "the Krihelinator"
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/www/lib/krihelinator/web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.PageView do
2 | use Krihelinator.Web, :view
3 |
4 | @doc """
5 | Takes a repo name and split and bold it like the repo names in github
6 | trending.
7 | """
8 | def split_and_bold(string) do
9 | [prefix, suffix] = String.split(string, "/")
10 | {:safe, "#{prefix} / #{suffix}"}
11 | end
12 |
13 | @doc """
14 | Path to the language history page.
15 | """
16 | def language_history_path(conn, language) do
17 | language = URI.encode_www_form(language)
18 | page_path(conn, :languages_history) <> "?languages=[\"#{language}\"]"
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/www/lib/mix/tasks/krihelinator.import.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Krihelinator.Import do
2 | use Mix.Task
3 | import Mix.Ecto
4 |
5 | @moduledoc """
6 | Import data from a json file. It won't work if the DB is already populated.
7 | """
8 |
9 | @usage "Usage: mix krihelinator.import path/to/json/file"
10 |
11 | def run([filepath | args]) do
12 | if File.exists?(filepath) do
13 | import_data(filepath, args)
14 | else
15 | Mix.shell.error @usage
16 | end
17 | end
18 |
19 | def run(_) do
20 | Mix.shell.error @usage
21 | end
22 |
23 | def import_data(filepath, args) do
24 | content = File.read!(filepath)
25 |
26 | for repo <- parse_repo(args) do
27 | ensure_repo(repo, args)
28 | ensure_started(repo, [])
29 |
30 | Krihelinator.ImportExport.import_data(content, repo)
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/www/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :krihelinator,
6 | version: "0.0.1",
7 | elixir: "~> 1.5",
8 | elixirc_paths: elixirc_paths(Mix.env),
9 | compilers: [:phoenix] ++ Mix.compilers,
10 | build_embedded: Mix.env == :prod,
11 | start_permanent: Mix.env == :prod,
12 | aliases: aliases(),
13 | deps: deps()]
14 | end
15 |
16 | # Configuration for the OTP application.
17 | #
18 | # Type `mix help compile.app` for more information.
19 | def application do
20 | [mod: {Krihelinator, []},
21 | applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger,
22 | :phoenix_ecto, :postgrex, :httpoison, :big_query, :timex, :floki,
23 | :quantum]]
24 | end
25 |
26 | # Specifies which paths to compile per environment.
27 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
28 | defp elixirc_paths(_), do: ["lib", "web"]
29 |
30 | # Specifies your project dependencies.
31 | #
32 | # Type `mix help deps` for examples and options.
33 | defp deps do
34 | [
35 | {:phoenix, "~> 1.2.0"},
36 | {:phoenix_pubsub, "~> 1.0"},
37 | {:phoenix_ecto, "~> 3.1"},
38 | {:postgrex, ">= 0.0.0"},
39 | {:phoenix_html, "~> 2.6"},
40 | {:phoenix_live_reload, "~> 1.0", only: :dev},
41 | {:cowboy, "~> 1.0"},
42 | {:httpoison, "~> 0.11.0", override: true},
43 | {:poison, "~> 2.2", override: true},
44 | {:floki, "~> 0.9.0"},
45 | {:timex, "~> 3.1"},
46 | {:big_query, "~> 0.0.12"},
47 | {:credo, "~> 0.5.3", only: [:dev, :test]},
48 | {:quantum, "~> 1.9"},
49 | ]
50 | end
51 |
52 | # Aliases are shortcuts or tasks specific to the current project.
53 | # For example, to create, migrate and run the seeds file at once:
54 | #
55 | # $ mix ecto.setup
56 | #
57 | # See the documentation for `Mix` for more info on aliases.
58 | defp aliases do
59 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
60 | "ecto.reset": ["ecto.drop", "ecto.setup"],
61 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/www/mix.lock:
--------------------------------------------------------------------------------
1 | %{"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], []},
2 | "big_query": {:hex, :big_query, "0.0.12", "eb18de1271e74ec948ff02ef709d863b9441f7008f571836200b8e45f281656e", [:mix], [{:httpoison, "~> 0.11.1", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
3 | "bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []},
4 | "calendar": {:hex, :calendar, "0.17.2", "d6b7bccc29c72203b076d4e488d967780bf2d123a96fafdbf45746fdc2fa342c", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, optional: false]}]},
5 | "certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], []},
6 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], []},
7 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []},
8 | "cowboy": {:hex, :cowboy, "1.0.4", "a324a8df9f2316c833a470d918aaf73ae894278b8aa6226ce7a9bf699388f878", [:make, :rebar], [{:cowlib, "~> 1.0.0", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.0", [hex: :ranch, optional: false]}]},
9 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
10 | "credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]},
11 | "crontab": {:hex, :crontab, "1.0.0", "7192d6f284be82c2a984b323f14a9e3c89eb88dc971a85c72a6f243677c7bc2d", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 2.1", [hex: :ecto, optional: true]}, {:timex, "~> 3.0", [hex: :timex, optional: false]}]},
12 | "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
13 | "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
14 | "ecto": {:hex, :ecto, "2.1.0", "2e685a5f4f8e02cc48ed9fb8aa31cdd9e8046c20f495f009e5146af0b167b6f5", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]},
15 | "floki": {:hex, :floki, "0.9.0", "e952ca71a453f7827ab5405106ac8d9ac5c9602d18aa5d2d893e5b9944e2499e", [:mix], [{:mochiweb_html, "~> 2.15", [hex: :mochiweb_html, optional: false]}]},
16 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
17 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []},
18 | "hackney": {:hex, :hackney, "1.6.6", "5564b4695d48fd87859e9df77a7fa4b4d284d24519f0cd7cc898f09e8fbdc8a3", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]},
19 | "httpoison": {:hex, :httpoison, "0.11.0", "b9240a9c44fc46fcd8618d17898859ba09a3c1b47210b74316c0ffef10735e76", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]},
20 | "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []},
21 | "jose": {:hex, :jose, "1.8.3", "1285151e6f3b70aa8b60c27801aed6d20f7bb9e87116fd7d1e405ea450549aad", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, optional: false]}]},
22 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
23 | "mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []},
24 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
25 | "mochiweb_html": {:hex, :mochiweb_html, "2.15.0", "d7402e967d7f9f2912f8befa813c37be62d5eeeddbbcb6fe986c44e01460d497", [:rebar3], []},
26 | "phoenix": {:hex, :phoenix, "1.2.0", "1bdeb99c254f4c534cdf98fd201dede682297ccc62fcac5d57a2627c3b6681fb", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.1", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]},
27 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.1.0", "4d4a91efed6bd28de899a2f5caf27be0f6fd04d85c259d3ede8fe63d766e0687", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]},
28 | "phoenix_html": {:hex, :phoenix_html, "2.8.0", "777598a4b6609ad6ab8b180f7b25c9af2904644e488922bb9b9b03ce988d20b1", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]},
29 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.5", "829218c4152ba1e9848e2bf8e161fcde6b4ec679a516259442561d21fde68d0b", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]},
30 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.0", "c31af4be22afeeebfaf246592778c8c840e5a1ddc7ca87610c41ccfb160c2c57", [:mix], []},
31 | "plug": {:hex, :plug, "1.3.0", "6e2b01afc5db3fd011ca4a16efd9cb424528c157c30a44a0186bcc92c7b2e8f3", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]},
32 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []},
33 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []},
34 | "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
35 | "quantum": {:hex, :quantum, "1.9.1", "f0264945f243ed438ea3644a3e19f1e053b43b0aaed6b1da78b523d542c99a30", [:mix], [{:calendar, "~> 0.16", [hex: :calendar, optional: false]}, {:crontab, "~> 1.0.0", [hex: :crontab, optional: false]}]},
36 | "ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []},
37 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []},
38 | "timex": {:hex, :timex, "3.1.24", "d198ae9783ac807721cca0c5535384ebdf99da4976be8cefb9665a9262a1e9e3", [:mix], [{:combine, "~> 0.7", [hex: :combine, optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, optional: false]}]},
39 | "tzdata": {:hex, :tzdata, "0.5.12", "1c17b68692c6ba5b6ab15db3d64cc8baa0f182043d5ae9d4b6d35d70af76f67b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, optional: false]}]}}
40 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20160707200816_create_github_repo.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.CreateGithubRepo do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:repos) do
6 | add :user, :string
7 | add :repo, :string
8 | add :merged_pull_requests, :integer
9 | add :proposed_pull_requests, :integer
10 | add :closed_issues, :integer
11 | add :new_issues, :integer
12 | add :commits, :integer
13 |
14 | timestamps()
15 | end
16 |
17 | create unique_index(:repos, [:user, :repo], name: :user_repo)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20160714142255_repo_name_instead_of_user_and_repo_fields.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.RepoNameInsteadOfUserAndRepoFields do
2 | use Ecto.Migration
3 |
4 | def change do
5 |
6 | drop index(:repos, [:user, :repo], name: :user_repo) # previous unique_index
7 |
8 | alter table(:repos) do
9 | remove :user
10 | remove :repo
11 | add :name, :string
12 | end
13 |
14 | create unique_index(:repos, [:name])
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20160714153524_add_krihelimeter_field.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.AddKrihelimeterField do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | add :krihelimeter, :integer
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20160716170829_add_description_field.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.AddDescriptionField do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | add :description, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20160721085732_add_authors_field.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.AddAuthorsField do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | add :authors, :integer
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20160722120749_add_trending_field.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.AddTrendingField do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | add :trending, :boolean
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20160728094627_add_user_requested_field.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.AddUserRequestedField do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | add :user_requested, :boolean, default: false
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20160808171600_add_language_field.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.AddLanguageField do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | add :language, :string
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20161219220300_create_language_history.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.CreateLanguageHistory do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:languages_history) do
6 | add :name, :string
7 | add :krihelimeter, :integer
8 | add :num_of_repos, :integer
9 | add :timestamp, :utc_datetime
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20161220021211_add_forks_field.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.AddForksField do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | add :forks, :integer
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20161225142125_create_language.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.CreateLanguage do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:languages) do
6 | add :name, :string
7 | add :krihelimeter, :integer
8 | add :num_of_repos, :integer
9 |
10 | timestamps()
11 | end
12 | create unique_index(:languages, [:name])
13 |
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20161225144134_use_new_language_model.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.UseNewLanguageModel do
2 | use Ecto.Migration
3 |
4 | @moduledoc """
5 | The GithubRepo model has a language field, and the LanguageHistory has a
6 | name field. Both fields should be replaced with belong_to fields to the new
7 | Language model.
8 | Note that the old fields are not removed, to make sure no data is
9 | deleted. In the case of GithubRepo the old field is renamed to language_name.
10 | In the future it will be changed to a virtual field.
11 | """
12 |
13 | def up do
14 | # Keep the old GithubRepo.language to new field
15 | rename table(:repos), :language, to: :language_name
16 | # Add a reference for GithubRepo to the Language table
17 | alter table(:repos) do
18 | add :language_id, references(:languages)
19 | end
20 | create index(:repos, [:language_id])
21 | # Add a reference for LanguageHistory to the Language table
22 | alter table(:languages_history) do
23 | add :language_id, references(:languages)
24 | end
25 | create index(:languages_history, [:language_id])
26 | end
27 |
28 | def down do
29 | drop index(:repos, [:language_id])
30 | alter table(:repos) do
31 | remove :language_id
32 | end
33 | rename table(:repos), :language_name, to: :language
34 | drop index(:languages_history, [:language_id])
35 | alter table(:languages_history) do
36 | remove :language_id
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20161225205601_remove_the_repo_language_name_field.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.RemoveTheRepoLanguageNameField do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | remove :language_name
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20170204024823_add_dirty_bit.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.AddDirtyBit do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | add :dirty, :boolean
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20170218222859_create_showcase.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.CreateShowcase do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:showcases) do
6 | add :name, :string
7 | add :href, :string
8 |
9 | timestamps()
10 | end
11 | create unique_index(:showcases, [:name])
12 | create unique_index(:showcases, [:href])
13 |
14 | alter table(:repos) do
15 | add :showcase_id, references(:showcases)
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20170219105003_add_showcase_description.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.AddShowcaseDescription do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:showcases) do
6 | add :description, :text
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20170411165434_unlimit_description_length.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.UnlimitDescriptionLength do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | modify :description, :text
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20170417211703_remove_num_of_repos.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.RemoveNumOfRepos do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:languages) do
6 | remove :num_of_repos
7 | end
8 |
9 | alter table(:languages_history) do
10 | remove :num_of_repos
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/www/priv/repo/migrations/20171104012807_remove_showcases.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Repo.Migrations.RemoveShowcase do
2 | use Ecto.Migration
3 |
4 | def change do
5 | alter table(:repos) do
6 | remove :showcase_id
7 | end
8 |
9 | drop table(:showcases)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/www/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # Krihelinator.Repo.insert!(%Krihelinator.SomeModel{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/www/priv/scripts/delete_zero_commits_repos.exs:
--------------------------------------------------------------------------------
1 | alias Krihelinator.{Repo, GithubRepo}
2 |
3 | for repo <- Repo.all(GithubRepo) do
4 | if repo.commits == 0 do
5 | IO.puts "#{repo.name} have zero commits, deleting!"
6 | Repo.delete!(repo)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/www/priv/scripts/remove_duplicate_history.exs:
--------------------------------------------------------------------------------
1 | # Due to restarts there is a chance for multiple language history points to
2 | # exist for the same date. They are redundant and slow down getting the history
3 | # page. This script remove the duplicates.
4 |
5 | defmodule HistoryDuplicatesCleaner do
6 | alias Krihelinator.{Repo, LanguageHistory}
7 | require Logger
8 |
9 | @moduledoc """
10 | A module to organize the duplication cleaning functions with meaningful names.
11 | """
12 |
13 | @doc """
14 | A datum is considered duplicate if there is already another datum for the same
15 | date (day) and language as this one. Therefore, remove it!
16 | """
17 | def remove_duplicates() do
18 | get_duplicates()
19 | |> Enum.each(&Repo.delete/1)
20 | end
21 |
22 | @doc """
23 | Get a list of all of the duplicates.
24 | """
25 | def get_duplicates() do
26 | LanguageHistory
27 | |> Repo.all
28 | |> log_length("history points")
29 | |> Enum.reduce({MapSet.new, []}, &filter_duplicates/2)
30 | |> elem(1) # 2nd element is the duplicates
31 | |> log_length("duplicates")
32 | end
33 |
34 | @doc """
35 | Should be used in reduce to check if for a datum we already have another
36 | one from the same date and language.
37 | """
38 | def filter_duplicates(datum, {date_language_set, duplicates}) do
39 | date = DateTime.to_date(datum.timestamp)
40 | language = datum.name
41 | date_language = {date, language}
42 | if MapSet.member?(date_language_set, date_language) do
43 | {date_language_set, [datum | duplicates]}
44 | else
45 | {MapSet.put(date_language_set, date_language), duplicates}
46 | end
47 | end
48 |
49 | def log_length(items, what) do
50 | num = length(items)
51 | Logger.info "Found #{num} #{what}"
52 | items
53 | end
54 | end
55 |
56 | HistoryDuplicatesCleaner.remove_duplicates()
57 |
--------------------------------------------------------------------------------
/www/priv/scripts/repos_to_csv.exs:
--------------------------------------------------------------------------------
1 | alias Krihelinator.{Repo, GithubRepo}
2 | import Ecto.Query, only: [from: 2]
3 | require Logger
4 |
5 | Logger.configure(level: :error)
6 |
7 |
8 | fields = ~w(name language merged_pull_requests proposed_pull_requests
9 | closed_issues new_issues commits authors)a
10 | fields
11 | |> Enum.join(",")
12 | |> IO.puts()
13 |
14 | query = from(r in GithubRepo, preload: :language)
15 | for repo <- Repo.all(query) do
16 | language = if repo.language, do: repo.language.name, else: ""
17 | repo = %{repo | language: language}
18 | fields
19 | |> Enum.map(fn field -> Map.fetch!(repo, field) end)
20 | |> Enum.join(",")
21 | |> IO.puts()
22 | end
23 |
--------------------------------------------------------------------------------
/www/priv/scripts/update_krihelimeter.exs:
--------------------------------------------------------------------------------
1 | # Run this script when there is a need to update the krihelimeter value
2 | # on all of the repos in the DB. For example: after changing the krihelimeter
3 | # calculation.
4 |
5 | alias Krihelinator.{Repo, GithubRepo}
6 |
7 | repos = Repo.all(GithubRepo)
8 | for repo <- repos do
9 | repo |> GithubRepo.changeset |> Repo.update!
10 | end
11 |
--------------------------------------------------------------------------------
/www/priv/static/css/style.css:
--------------------------------------------------------------------------------
1 | /* For fixed navbar */
2 | body {
3 | padding-top: 80px;
4 | }
5 |
6 | /* Phoenix flash messages */
7 | .alert:empty { display: none; }
8 |
9 | .badge-item {
10 | margin: 0 0 4px;
11 | }
12 |
13 | .krihelimeter-badge {
14 | display: flex;
15 | float: left;
16 | padding-right: 4px;
17 | }
18 |
19 | .label {
20 | display: inline-block;
21 | height: 20px;
22 | padding-top: .6em;
23 | }
24 |
25 | .repo-description {
26 | padding-top: 10px;
27 | }
28 |
29 | .stat-block {
30 | border: 1px solid #eee;
31 | text-align: center;
32 | padding: 20px;
33 | }
34 |
35 | .stat-block:hover {
36 | background-color: #f9f9f9;
37 | }
38 |
39 | .stat-block-text {
40 | color: #767676;
41 | }
42 |
43 | .navbar-header {
44 | min-height: 60px;
45 | }
46 |
47 | .navbar-nav > li > a {
48 | padding-top: 0px;
49 | padding-bottom: 0px;
50 | line-height: 60px;
51 | }
52 |
53 | nav img {
54 | height: 42px;
55 | margin-top: 10px;
56 | }
57 |
58 | nav .input-group {
59 | margin-top: 5px;
60 | }
61 |
62 | .header {
63 | padding-top: 10px;
64 | padding-bottom: 50px;
65 | text-align: center;
66 | }
67 |
68 | .header img {
69 | margin-top: 10px;
70 | margin-bottom: 10px;
71 | width: 100%;
72 | }
73 |
74 | .modal img {
75 | width: 100%;
76 | }
77 |
78 | .tile {
79 | display: inline-block;
80 | width: 100%;
81 | }
82 |
83 | .tiles {
84 | /* column width */
85 | -moz-column-width: 19em;
86 | -webkit-column-width: 19em;
87 |
88 | /* space between columns */
89 | -moz-column-gap: 1em;
90 | -webkit-column-gap: 1em;
91 | }
92 |
93 | .wide-input-group {
94 | width: 100%;
95 | margin-bottom: 1em;
96 | }
97 |
98 | /* Costumizations for desktop. Mobile first! */
99 | @media (min-width: 768px) {
100 | .container {
101 | max-width: 850px;
102 | }
103 |
104 | .krihelimeter-badge {
105 | float: right;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/www/priv/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagasaki45/krihelinator/243bfe476b8128dc2f0fcd913bebd8cf20b7deb6/www/priv/static/favicon.png
--------------------------------------------------------------------------------
/www/priv/static/js/app.js:
--------------------------------------------------------------------------------
1 | new Vue({
2 | el: '#languages-app',
3 | data: {
4 | checkedLanguages: []
5 | },
6 | methods: {
7 | see_history: function() {
8 | var json = encodeURIComponent(JSON.stringify(this.checkedLanguages));
9 | window.location.href = 'languages-history?languages=' + json;
10 | }
11 | }
12 | })
13 |
14 | new List(
15 | 'languages-app',
16 | {
17 | valueNames: ['name']
18 | }
19 | );
20 |
--------------------------------------------------------------------------------
/www/priv/static/loaderio-af9cda539c1b3a4a235147af21f0fe5d.txt:
--------------------------------------------------------------------------------
1 | loaderio-af9cda539c1b3a4a235147af21f0fe5d
--------------------------------------------------------------------------------
/www/priv/static/media/badge_demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagasaki45/krihelinator/243bfe476b8128dc2f0fcd913bebd8cf20b7deb6/www/priv/static/media/badge_demo.png
--------------------------------------------------------------------------------
/www/priv/static/media/logo_navbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nagasaki45/krihelinator/243bfe476b8128dc2f0fcd913bebd8cf20b7deb6/www/priv/static/media/logo_navbar.png
--------------------------------------------------------------------------------
/www/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 |
--------------------------------------------------------------------------------
/www/test/controllers/badge_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.BadgeControllerTest do
2 | use Krihelinator.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get conn, "/badge/nAgAsAkI45/krihelinator"
6 | assert response(conn, 200)
7 | end
8 |
9 | end
10 |
--------------------------------------------------------------------------------
/www/test/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.PageControllerTest do
2 | use Krihelinator.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get conn, "/"
6 | assert html_response(conn, 200) =~ "Krihelinator"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/www/test/fixtures/dump_sample.json:
--------------------------------------------------------------------------------
1 | [{"model":"Elixir.Krihelinator.Github.Language","items":[{"name":"TypeScript","id":7, "krihelimeter": 2500},{"name":"Ruby","id":19, "krihelimeter": 3000}]},{"model":"Elixir.Krihelinator.Github.Showcase","items":[{"name":"Swift","id":18,"href":"swift","description":null},{"name":"Serverless Architecture","id":21,"href":"serverless-architecture","description":"Build powerful, event-driven, serverless architectures with these open-source libraries and frameworks."},{"name":"Hacking Minecraft","id":23,"href":"hacking-minecraft","description":"Minecraft is a game about building blocks, but it doesn’t end there. Take Minecraft further with some of the projects below, or dive into the code mines and 🔨 your own!"},{"name":"Productivity tools","id":34,"href":"productivity-tools","description":"Build software faster with fewer headaches."},{"name":"JavaScript game engines","id":42,"href":"javascript-game-engines","description":"Learn or level up your L337 game dev skills and build amazing games together for web, desktop, or mobile."},{"name":"Projects that power GitHub for Mac","id":41,"href":"projects-that-power-github-for-mac","description":"Here are some of the great open source projects used within GitHub for Mac."},{"name":"Game Engines","id":17,"href":"game-engines","description":"Frameworks for building games across multiple platforms."},{"name":"Debug Politics","id":1,"href":"debug-politics","description":"Debug Politics is a hackathon for anyone dissatisfied with the state of our politics. Transform your dissatisfaction into a unique idea. Let’s use our talents as developers, designers, marketers, entrepreneurs, etc. to find innovative ways to make a real difference.\nHave a look through the projec..."},{"name":"Programming languages","id":2,"href":"programming-languages","description":"A list of programming languages that are actively developed on GitHub."},{"name":"Web accessibility","id":3,"href":"web-accessibility","description":"Tools to help you design and develop web projects with accessibility in mind."},{"name":"Projects with great wikis","id":4,"href":"projects-with-great-wikis","description":"These projects all use GitHub Wikis to share documentation and helpful resources."},{"name":"Music","id":5,"href":"music","description":"Drop the code bass with these musically themed repositories."},{"name":"Software Defined Radio","id":6,"href":"software-defined-radio","description":"Interested in Software for Wireless Communications? This is the place."},{"name":"Government apps","id":7,"href":"government","description":"Sites, apps, and tools built by governments across the world to make government work better, together. Read more at government.github.com."},{"name":"GitHub Browser Extensions","id":8,"href":"github-browser-extensions","description":"Some useful and fun browser extensions to augment / personalize the GitHub browser experience."},{"name":"Projects that power GitHub","id":9,"href":"projects-that-power-github","description":"Here are some of the great open source projects that GitHub is using to power its infrastructure."},{"name":"Virtual Reality","id":10,"href":"virtual-reality","description":"Immerse yourself in code and alternate realities with these cool VR tools."},{"name":"Great for new contributors","id":11,"href":"great-for-new-contributors","description":"These projects have a history and reputation for being welcoming to new open source contributors. Have you had a great experience as a new contributor on an open source project? We'd love to hear about it!"},{"name":"Social Impact","id":12,"href":"social-impact","description":"Open source projects that are making the world a better place."},{"name":"Data visualization","id":13,"href":"data-visualization","description":"Data visualization tools for the web."},{"name":"Science","id":14,"href":"science","description":"Scientists around the world are working together to solve some of the biggest questions in research. Take a look at some of the examples featured here to find out more."},{"name":"DevOps tools","id":15,"href":"devops-tools","description":"These tools help you manage servers and deploy happier and more often with more confidence."},{"name":"Web application frameworks","id":16,"href":"web-application-frameworks","description":"Need a website fast? These server-side frameworks will help you out."},{"name":"Open data","id":19,"href":"open-data","description":"Examples of using GitHub to store, publish, and collaborate on open, machine-readable datasets."},{"name":"Fonts","id":20,"href":"fonts","description":"Some of the world's best open source fonts are hosted right here on GitHub. For all you typographers, here's a few of our favorites. These are text based fonts only, so if you're looking for icon fonts, check out the icon font showcase."},{"name":"Text editors","id":22,"href":"text-editors","description":"The text editor is a sacred application for developers. Here's a showcase of some amazingly awesome open source editors."},{"name":"Fabric Mobile Developer Tools","id":24,"href":"fabric-mobile-developer-tools","description":"Tools to help mobile app development teams build, grow, and monetize their apps with Fabric.io."},{"name":"Package managers","id":25,"href":"package-managers","description":"Across programming languages and platforms, these popular package managers make it easy to distribute reusable libraries and plugins."},{"name":"Clean code linters","id":26,"href":"clean-code-linters","description":"Make sure your code is style guide compliant with these essential code linters, many of which are already supported in your editor."},{"name":"Open Source Operating Systems","id":27,"href":"open-source-operating-systems","description":"Highlighting the cool and highly technical subset of the open source world, operating systems!"},{"name":"Machine learning","id":29,"href":"machine-learning","description":"Laying the foundations for Skynet"},{"name":"Front-end JavaScript frameworks","id":30,"href":"front-end-javascript-frameworks","description":"While the number of ways to organize JavaScript is almost infinite, here are some tools that help you build single-page applications."},{"name":"Made in Africa","id":32,"href":"made-in-africa","description":"African tech is booming – so here are just a few of the great open source projects driving the continent."},{"name":"GitHub Pages examples","id":38,"href":"github-pages-examples","description":"Fine examples of projects using GitHub Pages."},{"name":"Projects that power GitHub for Windows","id":39,"href":"projects-that-power-github-for-windows","description":"Here are some of the great open source projects that GitHub for Windows uses in its app."},{"name":"Web games","id":40,"href":"web-games","description":"Have some fun with these open source games."},{"name":"Open source organizations","id":44,"href":"open-source-organizations","description":"A showcase of organizations showcasing their open source projects."},{"name":"CSS preprocessors","id":46,"href":"css-preprocessors","description":"Developers use CSS preprocessors to build CSS faster. You can browse some of the most popular CSS preprocessors on GitHub below."},{"name":"3D modeling","id":50,"href":"3d-modeling","description":"Has your obsession with 3D modeling only grown since Max Headroom hit the scene? Check out these repositories for leveling up your 3D."},{"name":"NoSQL databases","id":51,"href":"nosql-databases","description":"Ain't no party like a NoSQL party!"},{"name":"Open Journalism","id":28,"href":"open-journalism","description":"See how publications and data-driven journalists use open source to power their newsroom and ensure information is reported fairly and accurately."},{"name":"Ember projects","id":31,"href":"ember-projects","description":"Popular open source projects powered by Ember."},{"name":"Emoji","id":33,"href":"emoji","description":"A picture is worth a thousand words 👓🐵🍢🍊🌅"},{"name":"Security","id":35,"href":"security","description":"Open source projects to help build and operate more secure systems, along with tools for security monitoring and incident response."},{"name":"Design essentials","id":36,"href":"design-essentials","description":"This collection of design libraries are the best on the web, and will complete your toolset for designing stunning products. 🎨"},{"name":"Universal 2nd Factor","id":37,"href":"universal-2nd-factor","description":"Universal 2nd Factor (U2F) is an open authentication standard that provides users a stronger, easier to use, form of two-factor authentication. To jumpstart implementing U2F into your own websites or services, checkout the projects below.\n\nLearn more about the U2F specification from the FIDO Alli..."},{"name":"Game off winners","id":43,"href":"game-off-winners","description":"The winners of our annual game jam, where contestants have 1 month to build an open source game that is playable in the web browser."},{"name":"Policies","id":45,"href":"policies","description":"From federal governments to corporations to student clubs, groups of all sizes are using GitHub to share, discuss, and improve laws. Ask not what the repository can do for you..."},{"name":"Icon fonts","id":47,"href":"icon-fonts","description":"All the icons you could dream of all wrapped up nice and neatly as web fonts."},{"name":"Video tools","id":48,"href":"video-tools","description":"Alfred Hitchcock would be proud of these repositories. Get your fill of video resources for doing all types of video code procedures."},{"name":"Writing","id":49,"href":"writing","description":"GitHub repositories are places where writers can share their work with the world and solicit feedback, while others can fork the story and make their own variation. Here are some examples of researchers, textbook authors, event organizers, and novelists using GitHub for the written word."},{"name":"Software development tools","id":52,"href":"software-development-tools","description":"Build apps better, faster, stronger."},{"name":"Open Source Integrations","id":53,"href":"open-source-integrations","description":"Tools in the GitHub Integrations Directory that are open source software."},{"name":"Tools for Open Source","id":54,"href":"tools-for-open-source","description":"Software to make running your open source project a little bit easier."}]},{"model":"Elixir.Krihelinator.Github.Repo","items":[{"user_requested":false,"trending":true,"proposed_pull_requests":62,"new_issues":157,"name":"tootsuite/mastodon","merged_pull_requests":234,"language_id":19,"id":747435,"description":"A GNU Social-compatible microblogging server","commits":248,"closed_issues":148,"authors":98},{"user_requested":true,"trending":false,"proposed_pull_requests":10,"new_issues":259,"name":"Microsoft/vscode","merged_pull_requests":31,"language_id":7,"id":21729,"description":"Visual Studio Code","commits":184,"closed_issues":453,"authors":24},{"user_requested":false,"trending":false,"proposed_pull_requests":0,"new_issues":0,"name":"CocoaPods/Specs","merged_pull_requests":0,"language_id":null,"id":3295,"description":"The CocoaPods Master Repo","commits":1721,"closed_issues":0,"authors":718},{"user_requested":false,"trending":false,"proposed_pull_requests":15,"new_issues":19,"name":"githubschool/open-enrollment-classes-introduction-to-github","merged_pull_requests":78,"language_id":19,"id":441075,"description":"open-enrollment-classes-introduction-to-github created by GitHub Classroom","commits":117,"closed_issues":374,"authors":92}]},{"model":"Elixir.Krihelinator.History.Language","items":[{"timestamp":"2016-12-25T20:48:49.366300Z","language_id":7,"krihelimeter":36589,"id":7696},{"timestamp":"2016-12-25T20:48:49.756775Z","language_id":19,"krihelimeter":57184,"id":7812},{"timestamp":"2016-12-26T00:46:32.123587Z","language_id":7,"krihelimeter":36589,"id":7821},{"timestamp":"2016-12-26T00:46:32.512535Z","language_id":19,"krihelimeter":57184,"id":7937},{"timestamp":"2016-12-28T16:06:05.111540Z","language_id":7,"krihelimeter":28034,"id":7946},{"timestamp":"2016-12-28T16:06:05.526814Z","language_id":19,"krihelimeter":47177,"id":8070},{"timestamp":"2016-12-29T03:01:47.186895Z","language_id":7,"krihelimeter":26873,"id":8080},{"timestamp":"2016-12-29T03:01:48.884277Z","language_id":19,"krihelimeter":45044,"id":8196},{"timestamp":"2016-12-31T01:04:26.118908Z","language_id":7,"krihelimeter":25514,"id":8206},{"timestamp":"2016-12-31T01:04:26.371963Z","language_id":19,"krihelimeter":36808,"id":8299},{"timestamp":"2016-12-31T16:05:56.248575Z","language_id":7,"krihelimeter":25840,"id":8339},{"timestamp":"2016-12-31T16:05:56.466635Z","language_id":19,"krihelimeter":35875,"id":8423},{"timestamp":"2017-01-01T03:01:49.287488Z","language_id":7,"krihelimeter":25195,"id":8471},{"timestamp":"2017-01-01T03:01:50.616492Z","language_id":19,"krihelimeter":38707,"id":8548},{"timestamp":"2017-01-03T16:05:56.683766Z","language_id":7,"krihelimeter":25198,"id":8611},{"timestamp":"2017-01-03T16:05:56.826645Z","language_id":19,"krihelimeter":41706,"id":8647},{"timestamp":"2017-01-04T03:01:51.697933Z","language_id":7,"krihelimeter":25871,"id":8742},{"timestamp":"2017-01-04T03:01:52.112031Z","language_id":19,"krihelimeter":44260,"id":8778},{"timestamp":"2017-01-06T16:05:57.291423Z","language_id":7,"krihelimeter":30701,"id":8877},{"timestamp":"2017-01-06T16:05:57.720517Z","language_id":19,"krihelimeter":46324,"id":8982},{"timestamp":"2017-01-07T03:01:53.509708Z","language_id":7,"krihelimeter":31948,"id":9021},{"timestamp":"2017-01-07T03:01:54.964557Z","language_id":19,"krihelimeter":47208,"id":9116},{"timestamp":"2017-01-09T16:05:57.935178Z","language_id":7,"krihelimeter":33364,"id":9150},{"timestamp":"2017-01-09T16:05:58.138709Z","language_id":19,"krihelimeter":61814,"id":9208},{"timestamp":"2017-01-10T03:01:55.620711Z","language_id":7,"krihelimeter":35894,"id":9298},{"timestamp":"2017-01-10T03:01:55.952698Z","language_id":19,"krihelimeter":64398,"id":9345},{"timestamp":"2017-01-14T02:09:22.913708Z","language_id":7,"krihelimeter":44128,"id":9429},{"timestamp":"2017-01-14T02:09:23.031812Z","language_id":19,"krihelimeter":58312,"id":9448},{"timestamp":"2017-01-16T13:08:24.407296Z","language_id":7,"krihelimeter":43496,"id":9567},{"timestamp":"2017-01-16T13:08:24.866301Z","language_id":19,"krihelimeter":65601,"id":9684},{"timestamp":"2017-01-17T22:30:05.944996Z","language_id":7,"krihelimeter":44351,"id":9702},{"timestamp":"2017-01-17T22:30:06.746237Z","language_id":19,"krihelimeter":63029,"id":9817},{"timestamp":"2017-01-20T23:58:52.194687Z","language_id":7,"krihelimeter":41249,"id":9859},{"timestamp":"2017-01-20T23:58:53.207797Z","language_id":19,"krihelimeter":64759,"id":9949},{"timestamp":"2017-01-23T23:58:54.311161Z","language_id":7,"krihelimeter":41793,"id":10006},{"timestamp":"2017-01-23T23:58:55.228761Z","language_id":19,"krihelimeter":63375,"id":10083},{"timestamp":"2017-01-25T00:40:20.068165Z","language_id":7,"krihelimeter":43634,"id":10154},{"timestamp":"2017-01-25T00:40:20.511279Z","language_id":19,"krihelimeter":61136,"id":10234},{"timestamp":"2017-01-28T00:32:58.046345Z","language_id":7,"krihelimeter":39050,"id":10303},{"timestamp":"2017-01-28T00:32:59.104897Z","language_id":19,"krihelimeter":62971,"id":10377},{"timestamp":"2017-01-31T00:32:59.909612Z","language_id":7,"krihelimeter":42149,"id":10451},{"timestamp":"2017-01-31T00:33:01.025006Z","language_id":19,"krihelimeter":60375,"id":10525},{"timestamp":"2017-02-03T00:33:02.120395Z","language_id":7,"krihelimeter":36022,"id":10629},{"timestamp":"2017-02-04T17:50:25.756468Z","language_id":7,"krihelimeter":42170,"id":10733},{"timestamp":"2017-02-04T17:50:25.825831Z","language_id":19,"krihelimeter":70206,"id":10748},{"timestamp":"2017-02-08T00:36:35.252893Z","language_id":7,"krihelimeter":45544,"id":10891},{"timestamp":"2017-02-11T00:36:36.471963Z","language_id":19,"krihelimeter":66639,"id":11241},{"timestamp":"2017-02-11T00:36:36.227892Z","language_id":7,"krihelimeter":47823,"id":11164},{"timestamp":"2017-02-08T00:36:35.788084Z","language_id":19,"krihelimeter":68054,"id":11048},{"timestamp":"2017-02-13T16:01:56.791779Z","language_id":19,"krihelimeter":68017,"id":11451},{"timestamp":"2017-02-13T16:01:56.876889Z","language_id":7,"krihelimeter":46688,"id":11479},{"timestamp":"2017-02-14T22:36:12.206766Z","language_id":19,"krihelimeter":68210,"id":11691},{"timestamp":"2017-02-14T22:36:12.147308Z","language_id":7,"krihelimeter":45289,"id":11676},{"timestamp":"2017-02-17T22:35:28.784316Z","language_id":19,"krihelimeter":68821,"id":11805},{"timestamp":"2017-02-17T22:35:29.239910Z","language_id":7,"krihelimeter":44476,"id":11942},{"timestamp":"2017-02-19T13:08:50.895398Z","language_id":19,"krihelimeter":68599,"id":11981},{"timestamp":"2017-02-19T13:08:51.426109Z","language_id":7,"krihelimeter":44158,"id":12136},{"timestamp":"2017-02-22T13:01:08.051493Z","language_id":19,"krihelimeter":67133,"id":12258},{"timestamp":"2017-02-28T13:01:10.168126Z","language_id":19,"krihelimeter":71861,"id":12702},{"timestamp":"2017-02-22T13:01:08.498774Z","language_id":7,"krihelimeter":42732,"id":12369},{"timestamp":"2017-02-25T13:01:08.678199Z","language_id":19,"krihelimeter":69706,"id":12419},{"timestamp":"2017-02-28T13:01:10.719492Z","language_id":7,"krihelimeter":45641,"id":12890},{"timestamp":"2017-02-25T13:01:09.832386Z","language_id":7,"krihelimeter":42226,"id":12637},{"timestamp":"2017-03-03T13:01:11.176751Z","language_id":19,"krihelimeter":72826,"id":12980},{"timestamp":"2017-03-03T13:01:11.535261Z","language_id":7,"krihelimeter":45707,"id":13074},{"timestamp":"2017-03-06T01:38:24.200422Z","language_id":19,"krihelimeter":72501,"id":13179},{"timestamp":"2017-03-06T01:38:24.734020Z","language_id":7,"krihelimeter":46387,"id":13357},{"timestamp":"2017-03-09T01:37:08.815756Z","language_id":19,"krihelimeter":74994,"id":13466},{"timestamp":"2017-03-12T01:37:10.155733Z","language_id":7,"krihelimeter":51351,"id":13853},{"timestamp":"2017-03-12T01:37:09.620410Z","language_id":19,"krihelimeter":71938,"id":13654},{"timestamp":"2017-03-09T01:37:09.335734Z","language_id":7,"krihelimeter":45764,"id":13590},{"timestamp":"2017-03-15T02:35:23.014804Z","language_id":19,"krihelimeter":74391,"id":13952},{"timestamp":"2017-03-15T02:35:23.307288Z","language_id":7,"krihelimeter":50184,"id":14070},{"timestamp":"2017-03-18T00:12:06.174774Z","language_id":19,"krihelimeter":75869,"id":14135},{"timestamp":"2017-03-18T00:12:06.867783Z","language_id":7,"krihelimeter":45730,"id":14338},{"timestamp":"2017-03-21T18:40:08.043492Z","language_id":19,"krihelimeter":74809,"id":14425},{"timestamp":"2017-03-21T18:40:08.484363Z","language_id":7,"krihelimeter":44675,"id":14569},{"timestamp":"2017-03-24T18:40:08.756512Z","language_id":19,"krihelimeter":76797,"id":14629},{"timestamp":"2017-03-24T18:40:09.543479Z","language_id":7,"krihelimeter":48054,"id":14838},{"timestamp":"2017-03-27T00:22:05.545126Z","language_id":7,"krihelimeter":47186,"id":15083},{"timestamp":"2017-03-27T00:22:05.378237Z","language_id":19,"krihelimeter":78273,"id":14965},{"timestamp":"2017-03-30T09:09:04.939560Z","language_id":7,"krihelimeter":51734,"id":15276},{"timestamp":"2017-03-30T09:09:05.008516Z","language_id":19,"krihelimeter":78584,"id":15294},{"timestamp":"2017-04-02T09:09:07.177693Z","language_id":19,"krihelimeter":75684,"id":15603},{"timestamp":"2017-04-02T09:09:07.079718Z","language_id":7,"krihelimeter":50585,"id":15584},{"timestamp":"2017-04-15T17:48:00.530243Z","language_id":7,"krihelimeter":44613,"id":15815},{"timestamp":"2017-04-15T17:48:00.571971Z","language_id":19,"krihelimeter":77703,"id":15835},{"timestamp":"2017-04-17T11:25:07.775810Z","language_id":7,"krihelimeter":44613,"id":16071},{"timestamp":"2017-04-17T11:25:07.822418Z","language_id":19,"krihelimeter":77703,"id":16091},{"timestamp":"2017-04-17T14:41:29.587625Z","language_id":7,"krihelimeter":44613,"id":16327},{"timestamp":"2017-04-17T14:41:29.649158Z","language_id":19,"krihelimeter":77703,"id":16347}]}]
2 |
--------------------------------------------------------------------------------
/www/test/github/language_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.LanguageTest do
2 | use Krihelinator.ModelCase
3 |
4 | alias Krihelinator.Github.Language
5 |
6 | @valid_attrs %{krihelimeter: 42, name: "some content"}
7 | @invalid_attrs %{}
8 |
9 | test "changeset with valid attributes" do
10 | changeset = Language.changeset(%Language{}, @valid_attrs)
11 | assert changeset.valid?
12 | end
13 |
14 | test "changeset with invalid attributes" do
15 | changeset = Language.changeset(%Language{}, @invalid_attrs)
16 | refute changeset.valid?
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/www/test/github/repo_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.GithubRepoTest do
2 | use Krihelinator.ModelCase
3 |
4 | alias Krihelinator.Github.Repo
5 |
6 | @valid_attrs %{closed_issues: 42, commits: 42, merged_pull_requests: 42,
7 | new_issues: 42, proposed_pull_requests: 42, authors: 42,
8 | name: "some content"}
9 |
10 | test "changeset with valid attributes" do
11 | changeset = Repo.changeset(%Repo{}, @valid_attrs)
12 | assert changeset.valid?
13 | end
14 |
15 | test "check krihelimeter in changes from params" do
16 | changeset = Repo.changeset(%Repo{}, @valid_attrs)
17 | assert Map.has_key?(changeset.changes, :krihelimeter)
18 | end
19 |
20 | test "check krihelimeter in changes from model" do
21 | model = Map.merge(%Repo{}, @valid_attrs)
22 | changeset = Repo.changeset(model, %{})
23 | assert Map.has_key?(changeset.changes, :krihelimeter)
24 | end
25 |
26 | test "unlimited description length" do
27 | description = String.duplicate("a", 257)
28 | changes = Map.put(@valid_attrs, :description, description)
29 | {:ok, model} =
30 | %Repo{}
31 | |> Repo.changeset(changes)
32 | |> Krihelinator.Repo.insert()
33 | assert model.description == description
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/www/test/history/language_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.LanguageHistoryTest do
2 | use Krihelinator.ModelCase
3 |
4 | alias Krihelinator.History.Language
5 |
6 | @valid_attrs %{krihelimeter: 42, timestamp: DateTime.utc_now()}
7 | @invalid_attrs %{}
8 |
9 | test "changeset with valid attributes" do
10 | changeset = Language.changeset(%Language{}, @valid_attrs)
11 | assert changeset.valid?
12 | end
13 |
14 | test "changeset with invalid attributes" do
15 | changeset = Language.changeset(%Language{}, @invalid_attrs)
16 | refute changeset.valid?
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/www/test/import_export_test.exs:
--------------------------------------------------------------------------------
1 | # Some helper modules
2 |
3 | defmodule Person do
4 | defstruct name: nil, age: nil
5 | end
6 |
7 | defmodule FakedRepo do
8 | def all(Person) do
9 | [
10 | %Person{name: "moshe", age: 25},
11 | %Person{name: "jacob", age: 35},
12 | %Person{name: "yossi", age: 45},
13 | ]
14 | end
15 | end
16 |
17 |
18 | defmodule Krihelinator.ImportExportTest do
19 | use Krihelinator.ModelCase, async: true
20 | import Krihelinator.ImportExport
21 |
22 | test "export_data" do
23 | json = export_data([Person], FakedRepo)
24 | list = Poison.decode!(json)
25 | [%{"model" => model_name, "items" => items}] = list
26 | assert model_name == "Elixir.Person"
27 | first_person = hd(items)
28 | assert first_person["name"] == "moshe"
29 | assert first_person["age"] == 25
30 | end
31 |
32 | test "export_krihelinator_data has data for each model" do
33 | json = export_krihelinator_data()
34 | list = Poison.decode!(json)
35 | model_names =
36 | list
37 | |> Enum.map(fn %{"model" => model_name} -> model_name end)
38 | |> Enum.into(MapSet.new())
39 | for name <- ~w(Repo Language) do
40 | assert MapSet.member?(model_names, "Elixir.Krihelinator.Github." <> name)
41 | end
42 | assert MapSet.member?(model_names, "Elixir.Krihelinator.History.Language")
43 | end
44 |
45 | test "import fixture and then export create the same content" do
46 | fixture = Path.join(["test", "fixtures", "dump_sample.json"])
47 | in_json = File.read!(fixture)
48 | import_data(in_json, Krihelinator.Repo)
49 | out_json = export_krihelinator_data()
50 | in_map = Poison.decode!(in_json)
51 | out_map = Poison.decode!(out_json)
52 | for {{in_model, in_items}, {out_model, out_items}} <- Enum.zip(in_map, out_map) do
53 | assert in_model == out_model
54 | assert length(in_items) == length(out_items)
55 | in_items = Enum.sort_by(in_items, fn x -> x["name"] end)
56 | out_items = Enum.sort_by(out_items, fn x -> x["name"] end)
57 | for {in_item, out_item} <- Enum.zip(in_items, out_items) do
58 | assert in_item == out_item
59 | end
60 | end
61 | end
62 |
63 | test "seed, export, delete all, and make sure import still works" do
64 | alias Krihelinator.Github, as: GH
65 | alias Krihelinator.History.Language, as: LanguageHistory
66 |
67 | # Seed
68 |
69 | elixir =
70 | %GH.Language{}
71 | |> GH.Language.changeset(%{name: "Elixir"})
72 | |> Krihelinator.Repo.insert!()
73 |
74 | repo_params = %{
75 | name: "my/repo", authors: 1, commits: 2,
76 | merged_pull_requests: 3, proposed_pull_requests: 4, closed_issues: 5,
77 | new_issues: 6, description: "my awesome project!", user_requested: true
78 | }
79 | %GH.Repo{}
80 | |> GH.Repo.changeset(repo_params)
81 | |> Ecto.Changeset.put_assoc(:language, elixir)
82 | |> Krihelinator.Repo.insert!()
83 |
84 | now = DateTime.utc_now()
85 | yesterday = Timex.shift(now, days: -1)
86 | for dt <- [now, yesterday] do
87 | %LanguageHistory{}
88 | |> LanguageHistory.changeset(%{krihelimeter: 100, timestamp: dt})
89 | |> Ecto.Changeset.put_assoc(:language, elixir)
90 | |> Krihelinator.Repo.insert!()
91 | end
92 |
93 | # Export
94 |
95 | json = export_krihelinator_data()
96 |
97 | # Delete all
98 |
99 | for model <- [GH.Repo, LanguageHistory, GH.Language] do
100 | Krihelinator.Repo.delete_all(model)
101 | end
102 |
103 | # Import
104 |
105 | import_data(json, Krihelinator.Repo)
106 |
107 | # Basic asserts
108 |
109 | histories = Krihelinator.Repo.all(LanguageHistory)
110 | assert length(histories) == 2
111 | [newer, older] = histories
112 | assert newer.timestamp == now
113 | assert older.timestamp == yesterday
114 |
115 | repo =
116 | GH.Repo
117 | |> Krihelinator.Repo.get_by(name: "my/repo")
118 | |> Krihelinator.Repo.preload(:language)
119 |
120 | assert repo.language.name == "Elixir"
121 | end
122 |
123 | test "insert new language succeeds after import. Bug #163" do
124 |
125 | # Reset the languages_id_seq to 1
126 | Ecto.Adapters.SQL.query(Krihelinator.Repo, "SELECT setval('languages_id_seq', 1);", [])
127 |
128 | # Import one language with id 2
129 | [
130 | %{
131 | model: "Elixir.Krihelinator.Github.Language",
132 | items: [
133 | %{id: 2, name: "MosheLang", krihelimeter: 1000}
134 | ]
135 | }
136 | ]
137 | |> Poison.encode!()
138 | |> import_data(Krihelinator.Repo)
139 |
140 | # Try to create a language with auto generated id
141 | jacob_lang_struct = %Krihelinator.Github.Language{name: "JacobLang"}
142 | {:ok, _struct} = Krihelinator.Repo.insert(jacob_lang_struct)
143 | end
144 | end
145 |
--------------------------------------------------------------------------------
/www/test/input_validator_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.InputValidatorTest do
2 | use Krihelinator.ConnCase, async: true
3 |
4 | import Krihelinator.InputValidator
5 |
6 | # Default values for most tests
7 | @params %{"name" => "moshe"}
8 | @field "name"
9 | @allowed ~w(moshe jacob)
10 | @default :jacob
11 |
12 | test "verify_field against allowed values" do
13 | assert verify_field(@params, @field, @allowed, @default) == {:ok, :moshe}
14 | end
15 |
16 | test "verify_field invalid value against allowed values" do
17 | params = %{"name" => "yossi"}
18 | {result, _whatever} = verify_field(params, @field, @allowed, @default)
19 | assert result == :error
20 | end
21 |
22 | test "verify_field field doesn't exist result with default" do
23 | field = "age"
24 | assert verify_field(@params, field, @allowed, @default) == {:ok, :jacob}
25 | end
26 |
27 | test "verify_field with parsing function instead of allowed value" do
28 | params = %{"data" => "{\"name\": \"moshe\", \"age\": 30}"}
29 | expected = %{"name" => "moshe", "age" => 30}
30 | result = verify_field(params, "data", &Poison.decode/1, %{})
31 | assert result == {:ok, expected}
32 | end
33 |
34 | test "verify_field with parsing function and wrong value" do
35 | params = %{"data" => "{\"name\": \"mo"}
36 | {result, _whatever} = verify_field(params, "data", &Poison.decode/1, %{})
37 | assert result == :error
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/www/test/periodic/github_trending_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.Periodic.GithubTrendingTest do
2 | use ExUnit.Case
3 | alias Krihelinator.Periodic.GithubTrending
4 |
5 | test "scraping the trending page return 25 items" do
6 | num_of_trending =
7 | GithubTrending.scrape()
8 | |> Enum.to_list()
9 | |> length
10 | assert num_of_trending == 25
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/www/test/scraper_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.ScraperTest do
2 | use ExUnit.Case
3 | alias Krihelinator.Scraper
4 | doctest Scraper
5 |
6 | test "parsing numbers with comma, larger than 999" do
7 | html = """
8 |
Some title
9 |
1,234 commits to all branches
10 | """
11 | css_selector = "div.commits"
12 | pattern = ~r/(?\d+) commits to all branches/
13 |
14 | parsed = Scraper.general_extractor(html, css_selector, pattern)
15 | assert parsed == 1234
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/www/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.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 | import 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 | use Phoenix.ConnTest
22 |
23 | alias Krihelinator.Repo
24 | import Ecto
25 | import Ecto.Changeset
26 | import Ecto.Query
27 |
28 | import Krihelinator.Web.Router.Helpers
29 |
30 | # The default endpoint for testing
31 | @endpoint Krihelinator.Web.Endpoint
32 | end
33 | end
34 |
35 | setup tags do
36 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Krihelinator.Repo)
37 |
38 | unless tags[:async] do
39 | Ecto.Adapters.SQL.Sandbox.mode(Krihelinator.Repo, {:shared, self()})
40 | end
41 |
42 | {:ok, conn: Phoenix.ConnTest.build_conn()}
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/www/test/support/model_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.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 Krihelinator.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query
24 | import Krihelinator.ModelCase
25 | end
26 | end
27 |
28 | setup tags do
29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Krihelinator.Repo)
30 |
31 | unless tags[:async] do
32 | Ecto.Adapters.SQL.Sandbox.mode(Krihelinator.Repo, {:shared, self()})
33 | end
34 |
35 | :ok
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/www/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start
2 |
3 | Ecto.Adapters.SQL.Sandbox.mode(Krihelinator.Repo, :manual)
4 |
5 |
--------------------------------------------------------------------------------
/www/test/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.ErrorViewTest do
2 | use Krihelinator.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(Krihelinator.ErrorView, "404.html", []) ==
9 | "Page not found"
10 | end
11 |
12 | test "render 500.html" do
13 | assert render_to_string(Krihelinator.ErrorView, "500.html", []) ==
14 | "Internal server error"
15 | end
16 |
17 | test "render any other" do
18 | assert render_to_string(Krihelinator.ErrorView, "505.html", []) ==
19 | "Internal server error"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/www/test/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.LayoutViewTest do
2 | use Krihelinator.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/www/test/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Krihelinator.PageViewTest do
2 | use Krihelinator.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------