├── .github └── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── .coveragerc ├── .dockerignore ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── backend │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── grafit │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── concept_extractor │ │ ├── README.md │ │ ├── __init__.py │ │ ├── extractor.py │ │ ├── resources │ │ │ ├── __init__.py │ │ │ └── grafit_public_article.csv │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_extractor.py │ ├── concept_runner.py │ ├── crawler │ │ ├── __init__.py │ │ ├── crawler.py │ │ ├── source.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_crawler.py │ ├── management │ │ └── commands │ │ │ ├── clear_neo4j.py │ │ │ └── install_labels.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_article.py │ │ ├── 0003_load_data.py │ │ ├── 0004_article_related.py │ │ ├── 0005_auto_20181029_1255.py │ │ ├── 0006_fix_ids.py │ │ ├── 0007_auto_20181109_1633.py │ │ ├── 0008_workspace.py │ │ ├── 0009_article_workspace.py │ │ ├── 0010_fix_ids2.py │ │ ├── 0011_remove_article_related.py │ │ ├── 0012_create_search_index.py │ │ ├── 0013_add_unique_to_searchindex.py │ │ ├── 0014_create_search_word_index.py │ │ ├── 0015_searchresult_searchword.py │ │ ├── 0016_add_webcontent_to_article.py │ │ ├── 0017_fix_null_search_index.py │ │ └── __init__.py │ ├── models.py │ ├── search │ │ ├── __init__.py │ │ ├── search.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_search.py │ ├── serializers.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_articles.py │ │ └── test_workspaces.py │ └── views.py ├── manage.py ├── wait_for_neo4j.py └── wait_for_postgres.py ├── docker-compose.build.yml ├── docker-compose.dev.yml ├── docker-compose.yml ├── frontend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── nginx.conf ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo_192.png │ ├── logo_512.png │ ├── logo_transparent.svg │ └── manifest.json └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── components │ ├── AlertSnack.jsx │ ├── article │ │ ├── ArticleList.jsx │ │ ├── RelatedArticleBadges.jsx │ │ ├── articledetail │ │ │ ├── ArticleCreate.jsx │ │ │ ├── ArticleDetail.jsx │ │ │ ├── ArticleEdit.jsx │ │ │ └── ArticleUpdate.jsx │ │ ├── relatedarticletags │ │ │ ├── RelatedTags.jsx │ │ │ └── style.css │ │ └── relatedarticletree │ │ │ ├── RelatedArticleTree.jsx │ │ │ ├── TreebeadHeader.jsx │ │ │ └── treebeardstyles.js │ ├── login │ │ ├── Login.css │ │ ├── Login.jsx │ │ └── Register.jsx │ ├── navigation │ │ └── Navigation.jsx │ ├── search │ │ ├── SearchResultPopover.jsx │ │ ├── Searchbar.css │ │ └── Searchbar.jsx │ └── workspace │ │ ├── CreateWorkspace.jsx │ │ ├── UserDropdown.jsx │ │ ├── Workspace.css │ │ ├── Workspace.jsx │ │ └── WorkspaceToggle.jsx │ ├── constants.js │ ├── index.css │ ├── index.js │ ├── serviceWorker.js │ └── services │ ├── APIService.js │ └── AuthService.js ├── mkdocs.yml ├── mkdocs ├── Dockerfile ├── docs │ ├── developerdoc │ │ ├── api │ │ │ └── authentication.md │ │ ├── ci.md │ │ ├── containers.md │ │ ├── neo4j.md │ │ └── quickstart.md │ ├── img │ │ ├── DockerDeployment.png │ │ ├── articlecreated.png │ │ ├── color_logo_transparent.png │ │ ├── createworkspace_form.png │ │ ├── createworkspace_start.png │ │ ├── createworkspace_success.png │ │ ├── inputconnection.png │ │ ├── inputlabel.png │ │ ├── labelgrouping.png │ │ ├── login.png │ │ ├── newarticleform.png │ │ ├── search.png │ │ └── signup.png │ ├── index.md │ └── userdoc │ │ ├── createarticle.md │ │ ├── createworkspace.md │ │ ├── index.md │ │ ├── search.md │ │ └── signup.md ├── mkdocs.yml └── requirements.txt └── readthedocs.yml /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Description of problem** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Solution** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Additional context** 14 | Add any other context or screenshots about the feature request here. 15 | 16 | **Criteria for acceptance** 17 | A clear and concise list of what needs to happen. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .pytest_cache 3 | *.pyc 4 | /backend/static 5 | *.sqlite3 6 | *.sqlite3-journal 7 | .idea 8 | .swp 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | python: 4 | - "3.6" 5 | services: 6 | - docker 7 | 8 | cache: 9 | - pip 10 | - directories: 11 | - frontend/node_modules 12 | 13 | env: 14 | - DOCKER_COMPOSE_VERSION=1.17.1 15 | 16 | before_install: 17 | - sudo rm /usr/local/bin/docker-compose 18 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 19 | - chmod +x docker-compose 20 | - sudo mv docker-compose /usr/local/bin 21 | - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | sudo bash" 22 | - docker -v 23 | - docker-compose -v 24 | 25 | install: 26 | - pip install codecov 27 | 28 | script: 29 | - docker-compose -f docker-compose.dev.yml build 30 | - docker-compose -f docker-compose.dev.yml run --rm backend bash -c "python wait_for_postgres.py && python wait_for_neo4j.py && coverage run ./manage.py test && coverage xml -i" 31 | - docker-compose -f docker-compose.dev.yml run -e CI=true --rm frontend npm test -- --coverage 32 | - docker-compose rm -f 33 | 34 | - docker-compose -f docker-compose.build.yml build 35 | 36 | - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 37 | 38 | - docker tag grafit-frontend:latest grafitio/grafit-frontend 39 | - docker push grafitio/grafit-frontend 40 | 41 | - docker tag grafit-backend:latest grafitio/grafit-backend 42 | - docker push grafitio/grafit-backend 43 | 44 | - docker tag grafit-docs:latest grafitio/grafit-docs 45 | - docker push grafitio/grafit-docs 46 | 47 | after_success: 48 | - cd backend && codecov -F backend && cd ../ 49 | - cd frontend && codecov -F frontend && cd ../ 50 | - fossa 51 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at grafitio@protonmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute :rocket: 2 | 3 | Thank you for contributing! 4 | 5 | By participating in this project, you agree to abide by the code of conduct. 6 | 7 | Before we can merge your Pull-Request here are some guidelines that you need to follow. 8 | These guidelines exist not to annoy you, but to keep the code base clean, unified and future proof. 9 | 10 | ## Unit-Tests :umbrella: 11 | 12 | Please try to add a test for your pull-request. 13 | This project uses [Django](https://www.djangoproject.com/) on the backend and you can run tests with the following command: 14 | ``` 15 | docker-compose -f docker-compose.dev.yml run --rm backend ./manage.py test 16 | ``` 17 | 18 | 19 | ## Documentation :notebook: 20 | User and developer documentation is available [here](https://grafit.readthedocs.io/en/latest/). 21 | UI-Guidelines are available [here](https://brandguidelines.logojoy.com/grafit-io). 22 | 23 | ## CI :construction_worker: 24 | 25 | We automatically run your pull request through [Travis CI](https://www.travis-ci.org) and [Codecov](https://codecov.io/). 26 | 27 | ## Issues and Bugs :bug: 28 | 29 | To create a new issue, you can use the GitHub issue tracking system. 30 | We use the [Issue.sh GitHub Extension](https://issue.sh/) to get a better Issue Board with Issue Dependencies. 31 | 32 | ## Getting merged :checkered_flag: 33 | 34 | Please allow us time to review your pull requests. 35 | We will give our best to review everything as fast as possible, but cannot always live up to our own expectations. 36 | 37 | We try to follow a coherent [commit message style](https://karma-runner.github.io/2.0/dev/git-commit-msg.html) laid out by Karama Runner. 38 | 39 | Pull requests without tests most probably will not be merged. 40 | Documentation PRs obviously do not require tests. 41 | 42 | Thank you very much again for your contribution! 43 | 44 | If you habe any questions, you can drop a mail to grafitio@protonmail.com 45 | 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 grafit.io 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 |  2 | 3 | [](https://travis-ci.com/grafit-io/grafit) 4 | [](https://codecov.io/gh/grafit-io/grafit) 5 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fgrafit-io%2Fgrafit?ref=badge_shield) 6 | [](https://readthedocs.org/projects/grafit/) 7 | 8 | grafit is an MIT-licensed web app that allows teams to store, share and search knowledge in an effective way. With intelligent relation detection, different data sources are presented and searchable at one central entry point. 9 | 10 | ## Features 11 | 12 | :memo: store text in nodes 13 | :collision: text is analysed and relations to other nodes are learned 14 | :satellite: urls within the text are crawled and the content is saved 15 | :mag: full-text search 16 | :busts_in_silhouette: workspaces for managing access to group of nodes 17 | 18 | ## Documentation :notebook: 19 | 20 | User and developer documentation is available [here](https://grafit.readthedocs.io/en/latest/). 21 | UI-Guidelines are available [here](https://brandguidelines.logojoy.com/grafit-io). 22 | 23 | ## Getting started 24 | 25 | Install Docker [Mac](https://docs.docker.com/docker-for-mac/install/) / [Windows](https://docs.docker.com/docker-for-windows/install/) 26 | 27 | Clone this repo and run: 28 | 29 | ```bash 30 | docker-compose up 31 | ``` 32 | 33 | Go to [http://localhost:8001](http://localhost:8001) to read the docs. 34 | 35 | Go to [http://localhost:3000](http://localhost:3000) to start the app. 36 | 37 | ## Contribute 38 | 39 | Berfore contributing, please have a look at our [Code Of Conduct](CODE_OF_CONDUCT.md) and the [Contributing Guidelines](CONTRIBUTING.md). 40 | 41 | ## License 42 | 43 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fgrafit-io%2Fgrafit?ref=badge_large) 44 | -------------------------------------------------------------------------------- /backend/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = backend,grafit 3 | omit = 4 | */tests/* 5 | */site-packages/* 6 | */migrations/* 7 | *wsgi.py -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .gitignore 8 | .env 9 | */bin 10 | */obj 11 | README.md 12 | LICENSE 13 | .vscode -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-slim as base 2 | 3 | LABEL Name=grafit Version=0.0.1 4 | EXPOSE 8000 5 | 6 | # Using pipenv: 7 | COPY ./Pipfile Pipfile 8 | COPY ./Pipfile.lock Pipfile.lock 9 | RUN pip install pipenv 10 | RUN pipenv install --system --deploy --ignore-pipfile 11 | RUN python -m textblob.download_corpora 12 | 13 | COPY . /code 14 | WORKDIR /code 15 | 16 | # Migrates the database, uploads staticfiles 17 | CMD python wait_for_postgres.py && \ 18 | python wait_for_neo4j.py && \ 19 | ./manage.py install_labels && \ 20 | ./manage.py migrate && \ 21 | ./manage.py collectstatic --noinput && \ 22 | ./manage.py runserver 0.0.0.0:8000 23 | 24 | FROM base as prod 25 | CMD python wait_for_postgres.py && \ 26 | python wait_for_neo4j.py && \ 27 | ./manage.py install_labels && \ 28 | ./manage.py migrate && \ 29 | ./manage.py collectstatic --noinput && \ 30 | gunicorn --bind 0.0.0.0:8000 --access-logfile - backend.wsgi:application 31 | -------------------------------------------------------------------------------- /backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [dev-packages] 7 | pytest = "*" 8 | pylint = "*" 9 | "autopep8" = "*" 10 | 11 | [packages] 12 | textblob = "*" 13 | djangorestframework = "*" 14 | django = "*" 15 | markdown = "*" 16 | django-filter = "*" 17 | gunicorn = "*" 18 | whitenoise = "*" 19 | "psycopg2-binary" = "*" 20 | django-cors-headers = "*" 21 | djangorestframework-timed-auth-token = "*" 22 | neomodel = "==3.2.9" 23 | coverage = "*" 24 | html2text = "*" 25 | requests = "*" 26 | 27 | [requires] 28 | python_version = "3.6" 29 | -------------------------------------------------------------------------------- /backend/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "515b3b538a316c8ebfb37ed4d6241c06f62637c65f449c475d6c149499e26624" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", 22 | "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" 23 | ], 24 | "version": "==2018.11.29" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "coverage": { 34 | "hashes": [ 35 | "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", 36 | "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", 37 | "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", 38 | "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", 39 | "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", 40 | "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", 41 | "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", 42 | "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", 43 | "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", 44 | "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", 45 | "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", 46 | "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", 47 | "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", 48 | "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", 49 | "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", 50 | "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", 51 | "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", 52 | "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", 53 | "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", 54 | "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", 55 | "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", 56 | "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", 57 | "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", 58 | "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", 59 | "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", 60 | "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", 61 | "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", 62 | "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", 63 | "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", 64 | "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", 65 | "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" 66 | ], 67 | "index": "pypi", 68 | "version": "==4.5.2" 69 | }, 70 | "django": { 71 | "hashes": [ 72 | "sha256:a32c22af23634e1d11425574dce756098e015a165be02e4690179889b207c7a8", 73 | "sha256:d6393918da830530a9516bbbcbf7f1214c3d733738779f06b0f649f49cc698c3" 74 | ], 75 | "index": "pypi", 76 | "version": "==2.1.5" 77 | }, 78 | "django-cors-headers": { 79 | "hashes": [ 80 | "sha256:5545009c9b233ea7e70da7dbab7cb1c12afa01279895086f98ec243d7eab46fa", 81 | "sha256:c4c2ee97139d18541a1be7d96fe337d1694623816d83f53cb7c00da9b94acae1" 82 | ], 83 | "index": "pypi", 84 | "version": "==2.4.0" 85 | }, 86 | "django-filter": { 87 | "hashes": [ 88 | "sha256:6f4e4bc1a11151178520567b50320e5c32f8edb552139d93ea3e30613b886f56", 89 | "sha256:86c3925020c27d072cdae7b828aaa5d165c2032a629abbe3c3a1be1edae61c58" 90 | ], 91 | "index": "pypi", 92 | "version": "==2.0.0" 93 | }, 94 | "djangorestframework": { 95 | "hashes": [ 96 | "sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136", 97 | "sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5" 98 | ], 99 | "index": "pypi", 100 | "version": "==3.9.0" 101 | }, 102 | "djangorestframework-timed-auth-token": { 103 | "hashes": [ 104 | "sha256:31a0c2757ef8dc3bf1ff50adeaa78a4ceffeb46d743ae0d611b69c487362c565" 105 | ], 106 | "index": "pypi", 107 | "version": "==1.3.0" 108 | }, 109 | "gunicorn": { 110 | "hashes": [ 111 | "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", 112 | "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" 113 | ], 114 | "index": "pypi", 115 | "version": "==19.9.0" 116 | }, 117 | "html2text": { 118 | "hashes": [ 119 | "sha256:490db40fe5b2cd79c461cf56be4d39eb8ca68191ae41ba3ba79f6cb05b7dd662", 120 | "sha256:627514fb30e7566b37be6900df26c2c78a030cc9e6211bda604d8181233bcdd4" 121 | ], 122 | "index": "pypi", 123 | "version": "==2018.1.9" 124 | }, 125 | "idna": { 126 | "hashes": [ 127 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 128 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 129 | ], 130 | "version": "==2.8" 131 | }, 132 | "markdown": { 133 | "hashes": [ 134 | "sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa", 135 | "sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c" 136 | ], 137 | "index": "pypi", 138 | "version": "==3.0.1" 139 | }, 140 | "neo4j-driver": { 141 | "hashes": [ 142 | "sha256:6a3872f28d34e711e2f6335ae3f4ff10bae338b4986d3f0fe46c592ca1820877" 143 | ], 144 | "version": "==1.6.2" 145 | }, 146 | "neomodel": { 147 | "hashes": [ 148 | "sha256:2f7cd8aa60a0069d3cde4938581bfafc690d03820b48cf93aa727b6ec4bd923b" 149 | ], 150 | "index": "pypi", 151 | "version": "==3.2.9" 152 | }, 153 | "neotime": { 154 | "hashes": [ 155 | "sha256:849f2e7a67204d67d4cb57c523b6e1cf4d36baf105197294fee6585e76ebc632" 156 | ], 157 | "version": "==1.0.0" 158 | }, 159 | "nltk": { 160 | "hashes": [ 161 | "sha256:286f6797204ffdb52525a1d21ec0a221ec68b8e3fa4f2d25f412ac8e63c70e8d" 162 | ], 163 | "version": "==3.4" 164 | }, 165 | "psycopg2-binary": { 166 | "hashes": [ 167 | "sha256:036bcb198a7cc4ce0fe43344f8c2c9a8155aefa411633f426c8c6ed58a6c0426", 168 | "sha256:1d770fcc02cdf628aebac7404d56b28a7e9ebec8cfc0e63260bd54d6edfa16d4", 169 | "sha256:1fdc6f369dcf229de6c873522d54336af598b9470ccd5300e2f58ee506f5ca13", 170 | "sha256:21f9ddc0ff6e07f7d7b6b484eb9da2c03bc9931dd13e36796b111d631f7135a3", 171 | "sha256:247873cda726f7956f745a3e03158b00de79c4abea8776dc2f611d5ba368d72d", 172 | "sha256:3aa31c42f29f1da6f4fd41433ad15052d5ff045f2214002e027a321f79d64e2c", 173 | "sha256:475f694f87dbc619010b26de7d0fc575a4accf503f2200885cc21f526bffe2ad", 174 | "sha256:4b5e332a24bf6e2fda1f51ca2a57ae1083352293a08eeea1fa1112dc7dd542d1", 175 | "sha256:570d521660574aca40be7b4d532dfb6f156aad7b16b5ed62d1534f64f1ef72d8", 176 | "sha256:59072de7def0690dd13112d2bdb453e20570a97297070f876fbbb7cbc1c26b05", 177 | "sha256:5f0b658989e918ef187f8a08db0420528126f2c7da182a7b9f8bf7f85144d4e4", 178 | "sha256:649199c84a966917d86cdc2046e03d536763576c0b2a756059ae0b3a9656bc20", 179 | "sha256:6645fc9b4705ae8fbf1ef7674f416f89ae1559deec810f6dd15197dfa52893da", 180 | "sha256:6872dd54d4e398d781efe8fe2e2d7eafe4450d61b5c4898aced7610109a6df75", 181 | "sha256:6ce34fbc251fc0d691c8d131250ba6f42fd2b28ef28558d528ba8c558cb28804", 182 | "sha256:73920d167a0a4d1006f5f3b9a3efce6f0e5e883a99599d38206d43f27697df00", 183 | "sha256:8a671732b87ae423e34b51139628123bc0306c2cb85c226e71b28d3d57d7e42a", 184 | "sha256:8d517e8fda2efebca27c2018e14c90ed7dc3f04d7098b3da2912e62a1a5585fe", 185 | "sha256:9475a008eb7279e20d400c76471843c321b46acacc7ee3de0b47233a1e3fa2cf", 186 | "sha256:96947b8cd7b3148fb0e6549fcb31258a736595d6f2a599f8cd450e9a80a14781", 187 | "sha256:abf229f24daa93f67ac53e2e17c8798a71a01711eb9fcdd029abba8637164338", 188 | "sha256:b1ab012f276df584beb74f81acb63905762c25803ece647016613c3d6ad4e432", 189 | "sha256:b22b33f6f0071fe57cb4e9158f353c88d41e739a3ec0d76f7b704539e7076427", 190 | "sha256:b3b2d53274858e50ad2ffdd6d97ce1d014e1e530f82ec8b307edd5d4c921badf", 191 | "sha256:bab26a729befc7b9fab9ded1bba9c51b785188b79f8a2796ba03e7e734269e2e", 192 | "sha256:daa1a593629aa49f506eddc9d23dc7f89b35693b90e1fbcd4480182d1203ea90", 193 | "sha256:dd111280ce40e89fd17b19c1269fd1b74a30fce9d44a550840e86edb33924eb8", 194 | "sha256:e0b86084f1e2e78c451994410de756deba206884d6bed68d5a3d7f39ff5fea1d", 195 | "sha256:eb86520753560a7e89639500e2a254bb6f683342af598088cb72c73edcad21e6", 196 | "sha256:ff18c5c40a38d41811c23e2480615425c97ea81fd7e9118b8b899c512d97c737" 197 | ], 198 | "index": "pypi", 199 | "version": "==2.7.6.1" 200 | }, 201 | "pytz": { 202 | "hashes": [ 203 | "sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9", 204 | "sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c" 205 | ], 206 | "version": "==2018.9" 207 | }, 208 | "requests": { 209 | "hashes": [ 210 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 211 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 212 | ], 213 | "index": "pypi", 214 | "version": "==2.21.0" 215 | }, 216 | "singledispatch": { 217 | "hashes": [ 218 | "sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c", 219 | "sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8" 220 | ], 221 | "version": "==3.4.0.3" 222 | }, 223 | "six": { 224 | "hashes": [ 225 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 226 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 227 | ], 228 | "version": "==1.12.0" 229 | }, 230 | "textblob": { 231 | "hashes": [ 232 | "sha256:574f30be68a5afcc9f0ccfad088394833343a611c4f0a20473622e07c02cac0f", 233 | "sha256:fe607b2d19b8cb490c09cc4b09e3454e08ce9860359cc3eabcc5c2f18eec856c" 234 | ], 235 | "index": "pypi", 236 | "version": "==0.15.2" 237 | }, 238 | "urllib3": { 239 | "hashes": [ 240 | "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", 241 | "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" 242 | ], 243 | "version": "==1.24.1" 244 | }, 245 | "whitenoise": { 246 | "hashes": [ 247 | "sha256:118ab3e5f815d380171b100b05b76de2a07612f422368a201a9ffdeefb2251c1", 248 | "sha256:42133ddd5229eeb6a0c9899496bdbe56c292394bf8666da77deeb27454c0456a" 249 | ], 250 | "index": "pypi", 251 | "version": "==4.1.2" 252 | } 253 | }, 254 | "develop": { 255 | "astroid": { 256 | "hashes": [ 257 | "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", 258 | "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" 259 | ], 260 | "version": "==2.1.0" 261 | }, 262 | "atomicwrites": { 263 | "hashes": [ 264 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", 265 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" 266 | ], 267 | "version": "==1.2.1" 268 | }, 269 | "attrs": { 270 | "hashes": [ 271 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 272 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 273 | ], 274 | "version": "==18.2.0" 275 | }, 276 | "autopep8": { 277 | "hashes": [ 278 | "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c" 279 | ], 280 | "index": "pypi", 281 | "version": "==1.4.3" 282 | }, 283 | "isort": { 284 | "hashes": [ 285 | "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", 286 | "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", 287 | "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" 288 | ], 289 | "version": "==4.3.4" 290 | }, 291 | "lazy-object-proxy": { 292 | "hashes": [ 293 | "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", 294 | "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", 295 | "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", 296 | "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", 297 | "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", 298 | "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", 299 | "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", 300 | "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", 301 | "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", 302 | "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", 303 | "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", 304 | "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", 305 | "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", 306 | "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", 307 | "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", 308 | "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", 309 | "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", 310 | "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", 311 | "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", 312 | "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", 313 | "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", 314 | "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", 315 | "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", 316 | "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", 317 | "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", 318 | "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", 319 | "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", 320 | "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", 321 | "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" 322 | ], 323 | "version": "==1.3.1" 324 | }, 325 | "mccabe": { 326 | "hashes": [ 327 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 328 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 329 | ], 330 | "version": "==0.6.1" 331 | }, 332 | "more-itertools": { 333 | "hashes": [ 334 | "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4", 335 | "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc", 336 | "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9" 337 | ], 338 | "version": "==5.0.0" 339 | }, 340 | "pluggy": { 341 | "hashes": [ 342 | "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", 343 | "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" 344 | ], 345 | "version": "==0.8.0" 346 | }, 347 | "py": { 348 | "hashes": [ 349 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", 350 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" 351 | ], 352 | "version": "==1.7.0" 353 | }, 354 | "pycodestyle": { 355 | "hashes": [ 356 | "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", 357 | "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" 358 | ], 359 | "version": "==2.4.0" 360 | }, 361 | "pylint": { 362 | "hashes": [ 363 | "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", 364 | "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" 365 | ], 366 | "index": "pypi", 367 | "version": "==2.2.2" 368 | }, 369 | "pytest": { 370 | "hashes": [ 371 | "sha256:3e65a22eb0d4f1bdbc1eacccf4a3198bf8d4049dea5112d70a0c61b00e748d02", 372 | "sha256:5924060b374f62608a078494b909d341720a050b5224ff87e17e12377486a71d" 373 | ], 374 | "index": "pypi", 375 | "version": "==4.1.0" 376 | }, 377 | "six": { 378 | "hashes": [ 379 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 380 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 381 | ], 382 | "version": "==1.12.0" 383 | }, 384 | "typed-ast": { 385 | "hashes": [ 386 | "sha256:0555eca1671ebe09eb5f2176723826f6f44cca5060502fea259de9b0e893ab53", 387 | "sha256:0ca96128ea66163aea13911c9b4b661cb345eb729a20be15c034271360fc7474", 388 | "sha256:16ccd06d614cf81b96de42a37679af12526ea25a208bce3da2d9226f44563868", 389 | "sha256:1e21ae7b49a3f744958ffad1737dfbdb43e1137503ccc59f4e32c4ac33b0bd1c", 390 | "sha256:37670c6fd857b5eb68aa5d193e14098354783b5138de482afa401cc2644f5a7f", 391 | "sha256:46d84c8e3806619ece595aaf4f37743083f9454c9ea68a517f1daa05126daf1d", 392 | "sha256:5b972bbb3819ece283a67358103cc6671da3646397b06e7acea558444daf54b2", 393 | "sha256:6306ffa64922a7b58ee2e8d6f207813460ca5a90213b4a400c2e730375049246", 394 | "sha256:6cb25dc95078931ecbd6cbcc4178d1b8ae8f2b513ae9c3bd0b7f81c2191db4c6", 395 | "sha256:7e19d439fee23620dea6468d85bfe529b873dace39b7e5b0c82c7099681f8a22", 396 | "sha256:7f5cd83af6b3ca9757e1127d852f497d11c7b09b4716c355acfbebf783d028da", 397 | "sha256:81e885a713e06faeef37223a5b1167615db87f947ecc73f815b9d1bbd6b585be", 398 | "sha256:94af325c9fe354019a29f9016277c547ad5d8a2d98a02806f27a7436b2da6735", 399 | "sha256:b1e5445c6075f509d5764b84ce641a1535748801253b97f3b7ea9d948a22853a", 400 | "sha256:cb061a959fec9a514d243831c514b51ccb940b58a5ce572a4e209810f2507dcf", 401 | "sha256:cc8d0b703d573cbabe0d51c9d68ab68df42a81409e4ed6af45a04a95484b96a5", 402 | "sha256:da0afa955865920edb146926455ec49da20965389982f91e926389666f5cf86a", 403 | "sha256:dc76738331d61818ce0b90647aedde17bbba3d3f9e969d83c1d9087b4f978862", 404 | "sha256:e7ec9a1445d27dbd0446568035f7106fa899a36f55e52ade28020f7b3845180d", 405 | "sha256:f741ba03feb480061ab91a465d1a3ed2d40b52822ada5b4017770dfcb88f839f", 406 | "sha256:fe800a58547dd424cd286b7270b967b5b3316b993d86453ede184a17b5a6b17d" 407 | ], 408 | "markers": "python_version < '3.7' and implementation_name == 'cpython'", 409 | "version": "==1.1.1" 410 | }, 411 | "wrapt": { 412 | "hashes": [ 413 | "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" 414 | ], 415 | "version": "==1.10.11" 416 | } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /backend/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/backend/__init__.py -------------------------------------------------------------------------------- /backend/backend/settings.py: -------------------------------------------------------------------------------- 1 | from neomodel import config 2 | """ 3 | Django settings for backend project. 4 | 5 | Generated by 'django-admin startproject' using Django 2.1.2. 6 | 7 | For more information on this file, see 8 | https://docs.djangoproject.com/en/2.1/topics/settings/ 9 | 10 | For the full list of settings and their values, see 11 | https://docs.djangoproject.com/en/2.1/ref/settings/ 12 | """ 13 | 14 | import os 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '0jhlvrzdzpugqw_zrdujuvz9(i&nwsh$#(lmgacnykiw6s1f=w' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ["*"] 29 | 30 | config.DATABASE_URL = 'bolt://neo4j:test@neo4j:7687' 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'rest_framework', 42 | 'timed_auth_token', 43 | 'corsheaders', 44 | 'grafit', 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'corsheaders.middleware.CorsMiddleware', 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | 'django.middleware.common.CommonMiddleware', 57 | 'whitenoise.middleware.WhiteNoiseMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'backend.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'backend.wsgi.application' 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.postgresql', 86 | 'NAME': 'postgres', 87 | 'USER': 'postgres', 88 | 'HOST': 'db', 89 | 'PORT': 5432, 90 | } 91 | } 92 | 93 | AUTH_USER_MODEL = "grafit.User" 94 | 95 | # Password validation 96 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 97 | 98 | AUTH_PASSWORD_VALIDATORS = [ 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 110 | }, 111 | ] 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | # Static files (CSS, JavaScript, Images) 127 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 128 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 129 | STATICFILES_DIRS = [] 130 | STATIC_URL = '/api/static/' 131 | STATICFILES_FINDERS = ( 132 | 'django.contrib.staticfiles.finders.FileSystemFinder', 133 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 134 | ) 135 | 136 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' 137 | 138 | # Django Rest Framework 139 | REST_FRAMEWORK = { 140 | 'DEFAULT_RENDERER_CLASSES': ( 141 | 'rest_framework.renderers.JSONRenderer', 142 | 'rest_framework.renderers.BrowsableAPIRenderer', 143 | ), 144 | 'DEFAULT_PERMISSION_CLASSES': [ 145 | 'rest_framework.permissions.IsAuthenticated', 146 | ], 147 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 148 | 'rest_framework.authentication.SessionAuthentication', 149 | 'timed_auth_token.authentication.TimedAuthTokenAuthentication', 150 | ), 151 | 'PAGE_SIZE': 25 152 | } 153 | 154 | CORS_ORIGIN_WHITELIST = ( 155 | 'localhost:3000' 156 | ) 157 | 158 | CORS_ALLOW_CREDENTIALS = True 159 | -------------------------------------------------------------------------------- /backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include, re_path, reverse_lazy 3 | from django.views.generic.base import RedirectView 4 | from grafit import views 5 | from rest_framework import routers 6 | from timed_auth_token.views import TimedAuthTokenCreateView 7 | 8 | router = routers.DefaultRouter() 9 | router.register(r'users', views.UserViewSet) 10 | router.register(r'users', views.UserCreateViewSet) 11 | router.register(r'groups', views.GroupViewSet) 12 | router.register(r'articles', views.ArticleViewSet, basename="article") 13 | router.register(r'articletitless', views.ArticleTitleViewSet, basename="articletitle") 14 | router.register(r'workspaces', views.WorkspaceViewSet, basename="workspace") 15 | router.register(r'search', views.SearchResultViewSet, basename="search") 16 | 17 | 18 | urlpatterns = [ 19 | path('api-token-auth/', TimedAuthTokenCreateView.as_view()), 20 | path('admin/', admin.site.urls), 21 | path('api/v1/', include(router.urls)), 22 | path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), 23 | path('api/v1/runner', views.ConceptRunnerAPI.as_view()), 24 | path('api/v1/hideconcept', views.HideConceptAPI.as_view()), 25 | path('api/v1/addconcept', views.AddConceptAPI.as_view()), 26 | 27 | # the 'api-root' from django rest-frameworks default router 28 | # http://www.django-rest-framework.org/api-guide/routers/#defaultrouter 29 | re_path(r'^$', RedirectView.as_view( 30 | url=reverse_lazy('api-root'), permanent=False)), 31 | ] 32 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/grafit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/__init__.py -------------------------------------------------------------------------------- /backend/grafit/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from .models import User, Article, Workspace 5 | 6 | 7 | @admin.register(User) 8 | class UserAdmin(UserAdmin): 9 | pass 10 | 11 | 12 | @admin.register(Article) 13 | class ArticleAdmin(admin.ModelAdmin): 14 | list_display = ('id', 'title', 'updated_at', 'created_at',) 15 | ordering = ('updated_at',) 16 | pass 17 | 18 | 19 | @admin.register(Workspace) 20 | class WorkspaceAdmin(admin.ModelAdmin): 21 | pass 22 | -------------------------------------------------------------------------------- /backend/grafit/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GrafitConfig(AppConfig): 5 | name = 'grafit' 6 | -------------------------------------------------------------------------------- /backend/grafit/concept_extractor/README.md: -------------------------------------------------------------------------------- 1 | # Concept Extractor 2 | 3 | Install Dependencies 4 | ``` 5 | pipenv install 6 | pipenv shell 7 | ``` 8 | 9 | Run Tests 10 | ``` 11 | PYTHONPATH=./concept_extractor pytest 12 | ``` -------------------------------------------------------------------------------- /backend/grafit/concept_extractor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/concept_extractor/__init__.py -------------------------------------------------------------------------------- /backend/grafit/concept_extractor/extractor.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import csv 3 | import math 4 | 5 | from pkg_resources import resource_string 6 | from textblob import TextBlob as tb 7 | 8 | from ..models import Article 9 | 10 | stopwords = set( 11 | ["’", "−", "—", "'s", "a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", 12 | "aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can't", 13 | "cannot", "could", "couldn't", "did", "didn't", "do", "does", "doesn't", "doing", "don't", "down", "during", 14 | "each", "few", "for", "from", "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having", "he", 15 | "he'd", "he'll", "he's", "her", "here", "here's", "hers", "herself", "him", "himself", "his", "how", "how's", "i", 16 | "i'd", "i'll", "i'm", "i've", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself", "let's", "me", 17 | "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", 18 | "on", "once", "only", "or", "other", "ought", "our", "ours" "ourselves", "out", "over", "own", "same", "shan't", 19 | "she", "she'd", "she'll", "she's", "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the", 20 | "their", "theirs", "them", "themselves", "then", "there", "there's", "these", "they", "they'd", "they'll", 21 | "they're", "they've", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasn't", 22 | "we", "we'd", "we'll", "we're", "we've", "were", "weren't", "what", "what's", "when", "when's", "where", "where's", 23 | "which", "while", "who", "who's", "whom", "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd", 24 | "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves"]) 25 | 26 | 27 | class ExtractStrategyAbstract(object): 28 | """Abstract strategy class for the extract method.""" 29 | 30 | __metaclass__ = abc.ABCMeta # define as absctract class 31 | 32 | @abc.abstractmethod 33 | def extract_keyphrases(self, text: str): 34 | """Required Method""" 35 | 36 | 37 | class FakeExtractStrategy(ExtractStrategyAbstract): 38 | def extract_keyphrases(self, text: str): 39 | return ["test"] 40 | 41 | 42 | class TextblobTfIdfKeyphraseExtractStrategy(ExtractStrategyAbstract): 43 | pass 44 | 45 | 46 | class TextblobTfIdfExtractStrategy(ExtractStrategyAbstract): 47 | 48 | def __init__(self, loadFromDB=True): 49 | if loadFromDB: 50 | self.corpus = self.load_corpus_db() 51 | else: 52 | self.corpus = self.load_corpus() 53 | 54 | def tf(self, word, blob): 55 | return blob.words.count(word) / len(blob.words) 56 | 57 | def n_containing(self, word, corpus): 58 | return sum(1 for blob in corpus if word in blob.words) 59 | 60 | def idf(self, word, corpus): 61 | return math.log(len(corpus) / (1 + self.n_containing(word, corpus))) 62 | 63 | def tfidf(self, word, blob, corpus): 64 | return self.tf(word, blob) * self.idf(word, corpus) 65 | 66 | def load_corpus(self): 67 | raw_csv = resource_string( 68 | 'grafit.concept_extractor.resources', 'grafit_public_article.csv').decode('utf-8').splitlines() 69 | results = list(csv.reader(raw_csv, delimiter=',')) 70 | 71 | tbs = [] 72 | for result in results: 73 | tbs.append(tb(result[1] + " " + result[2])) 74 | 75 | return tbs 76 | 77 | def load_corpus_db(self): 78 | articles = Article.objects.all() 79 | 80 | tbs = [] 81 | for article in articles: 82 | tbs.append(tb(article.title.lower() + " " + article.text.lower())) 83 | 84 | return tbs 85 | 86 | def extract_keyphrases(self, text: str, top_n_words=5): 87 | result = [] 88 | blob = tb(text.lower()) 89 | 90 | scores = {word: self.tfidf(word, blob, self.corpus) 91 | for word in blob.words} 92 | sorted_words = sorted(scores.items(), key=lambda x: x[1], reverse=True) 93 | for word, score in sorted_words: 94 | if word.lower() not in stopwords: 95 | result.append({ 96 | "word": word, 97 | "tf-idf": score 98 | }) 99 | 100 | return result[:top_n_words] 101 | -------------------------------------------------------------------------------- /backend/grafit/concept_extractor/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/concept_extractor/resources/__init__.py -------------------------------------------------------------------------------- /backend/grafit/concept_extractor/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/concept_extractor/tests/__init__.py -------------------------------------------------------------------------------- /backend/grafit/concept_extractor/tests/test_extractor.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from grafit.concept_extractor.extractor import FakeExtractStrategy, TextblobTfIdfExtractStrategy 3 | 4 | 5 | class ExtractorTest(TestCase): 6 | 7 | text = """ 8 | Vulkan Ollagüe (Spanish pronunciation: [oˈʝaɣwe]) or Ullawi 9 | (Aymara pronunciation: [uˈʎawi]) is a massive andesite stratovolcano Vulkan in the 10 | Andes on the border between Bolivia and Chile, within the Antofagasta Region of 11 | Chile and the Potosi Department of Bolivia. Part of the Central Volcanic Zone of the 12 | Andes, its highest summit is 5,868 metres (19,252 ft) above sea level and features 13 | a summit crater that opens to the south. The western rim of the summit crater is 14 | formed by a compound of lava domes, the youngest of which features a vigorous fumarole 15 | that is visible from afar. The Vulkan... 16 | """ 17 | 18 | def test_function(self): 19 | assert FakeExtractStrategy().extract_keyphrases("testtestest") == ["test"] 20 | 21 | def test_tfidf_strategy_from_db(self): 22 | # test load corpus 23 | TextblobTfIdfExtractStrategy() 24 | 25 | def test_tfidf_strategy_from_csv(self): 26 | TextblobTfIdfExtractStrategy(loadFromDB=False) 27 | 28 | def test_tfidf_strategy_top_word(self): 29 | keyword_extractor = TextblobTfIdfExtractStrategy(loadFromDB=False) 30 | keywords = keyword_extractor.extract_keyphrases(self.text) 31 | assert keywords[0]['word'] == "vulkan" 32 | 33 | def test_tfidf_strategy_nr_keywords(self): 34 | keyword_extractor = TextblobTfIdfExtractStrategy(loadFromDB=False) 35 | keywords = keyword_extractor.extract_keyphrases(self.text, top_n_words=10) 36 | assert len(keywords) == 10 37 | -------------------------------------------------------------------------------- /backend/grafit/concept_runner.py: -------------------------------------------------------------------------------- 1 | from .concept_extractor.extractor import TextblobTfIdfExtractStrategy 2 | from .models import Article, GraphArticle 3 | import logging 4 | 5 | 6 | logger = logging.getLogger() 7 | logger.setLevel(logging.INFO) 8 | logger.addHandler(logging.StreamHandler()) 9 | 10 | 11 | class ConceptRunner: 12 | 13 | _tfidf_extractor = None 14 | 15 | @classmethod 16 | def _get_tfidf_extractor(cls): 17 | if not cls._tfidf_extractor: 18 | cls._tfidf_extractor = TextblobTfIdfExtractStrategy() 19 | return cls._tfidf_extractor 20 | 21 | @classmethod 22 | def _extract_and_save(cls, article, disconnectAll=False): 23 | article_node = GraphArticle.nodes.get_or_none(uid=article.id) 24 | 25 | if not article_node: 26 | article_node = GraphArticle( 27 | uid=article.id, name=article.title).save() 28 | 29 | if disconnectAll: 30 | article_node.related.disconnect_all() 31 | 32 | keywords = cls._get_tfidf_extractor().extract_keyphrases(article.text) 33 | logger.info(f"Extracted keywords {keywords}") 34 | 35 | for keyword in keywords: 36 | 37 | related_title = keyword['word'] 38 | related_article = Article.objects.filter( 39 | title__iexact=related_title).first() 40 | 41 | if not related_article: 42 | related_article = Article(title=related_title, workspace=article.workspace) 43 | related_article.save() 44 | 45 | related_article_node = GraphArticle.nodes.get_or_none( 46 | uid=related_article.id) 47 | 48 | if not related_article_node: 49 | related_article_node = GraphArticle( 50 | uid=related_article.id, name=related_article.title) 51 | 52 | article_node.save() 53 | related_article_node.save() 54 | logger.info( 55 | f"Set {article_node.name} as related to {related_article_node.name}") 56 | 57 | if article_node.related.is_connected(related_article_node): 58 | rel = article_node.related.relationship(related_article_node) 59 | rel.tf_idf = keyword['tf-idf'] 60 | else: 61 | article_node.related.connect( 62 | related_article_node, {'tf_idf': keyword['tf-idf']}) 63 | 64 | @classmethod 65 | def generate_graph(cls): 66 | print("[ConceptRunner] generating graph for all articles") 67 | articles = Article.objects.all() 68 | 69 | for article in articles: 70 | cls._extract_and_save(article) 71 | 72 | @classmethod 73 | def generate_concepts_for_article(cls, articleId): 74 | article = Article.objects.get(pk=articleId) 75 | 76 | if article: 77 | cls._extract_and_save(article, disconnectAll=False) 78 | -------------------------------------------------------------------------------- /backend/grafit/crawler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/crawler/__init__.py -------------------------------------------------------------------------------- /backend/grafit/crawler/crawler.py: -------------------------------------------------------------------------------- 1 | from ..crawler.source import Source 2 | import logging 3 | import re 4 | import html2text 5 | 6 | logger = logging.getLogger() 7 | logger.setLevel(logging.INFO) 8 | 9 | 10 | class Crawler: 11 | 12 | def __init__(self): 13 | self.text_maker = html2text.HTML2Text() 14 | self.text_maker.ignore_links = True 15 | self.text_maker.skip_internal_links = True 16 | self.text_maker.bypass_tables = True 17 | self.text_maker.ignore_anchors = True 18 | self.text_maker.ignore_emphasis = True 19 | self.text_maker.ignore_tables = True 20 | self.text_maker.single_line_break = True 21 | self.text_maker.ignore_images = True 22 | 23 | def getWebContent(self, articleText: str) -> str: 24 | content = "" 25 | sources = Crawler.__extractURL(articleText) 26 | 27 | for source in sources: 28 | content += source.getContent(self.text_maker) 29 | return content 30 | 31 | @staticmethod 32 | def __extractURL(content: str) -> list: 33 | urlRegex = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+' 34 | urls = re.findall(urlRegex, content) 35 | return list({Source(url) for url in urls}) 36 | -------------------------------------------------------------------------------- /backend/grafit/crawler/source.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import html2text 3 | import requests 4 | 5 | logger = logging.getLogger() 6 | logger.setLevel(logging.INFO) 7 | 8 | class Source: 9 | def __init__(self, url: str): 10 | self.url = url 11 | 12 | def getContent(self, HTML2Text) -> str: 13 | logger.info(f"Crawling: {self.url}") 14 | try: 15 | request = requests.get(self.url, timeout=5) 16 | request.raise_for_status() 17 | 18 | content_type = request.headers.get('content-type') 19 | if 'application/pdf' in content_type: 20 | logger.info(f"Failed to crawl: Content type {content_type}") 21 | return "" 22 | if 'application' in content_type: 23 | logger.info(f"Failed to crawl: Content type {content_type}") 24 | return "" 25 | 26 | return HTML2Text.handle(request.text) 27 | except Exception: 28 | logger.info(f"Failed to crawl: {self.url}") 29 | 30 | return "" 31 | -------------------------------------------------------------------------------- /backend/grafit/crawler/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/crawler/tests/__init__.py -------------------------------------------------------------------------------- /backend/grafit/crawler/tests/test_crawler.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from grafit.crawler.crawler import Crawler 3 | 4 | 5 | class CrawlerTest(TestCase): 6 | def test_crawler_no_content(self): 7 | crawler = Crawler() 8 | webContent = crawler.getWebContent("Nothing with any URL") 9 | self.assertEqual(webContent, "") 10 | 11 | def test_crawler_not_reachable_url(self): 12 | crawler = Crawler() 13 | webContent = crawler.getWebContent("Not existing URL https://www.dauiwrbn239hraoiwezfh.com/alsdäflqwäekf and some more text") 14 | self.assertEqual(webContent, "") 15 | 16 | def test_extractUrl_none(self): 17 | text = "" 18 | sourceList = Crawler._Crawler__extractURL(text) 19 | self.assertFalse(sourceList) 20 | 21 | def test_extractUrl_signle(self): 22 | text = "https://grafit.io/" 23 | sourceList = Crawler._Crawler__extractURL(text) 24 | 25 | self.assertEqual(sourceList[0].url, "https://grafit.io/") 26 | 27 | def test_extractUrl_multiple(self): 28 | text = "Some content is https://grafit.io/ and http://www.hslu.ch" 29 | sourceList = Crawler._Crawler__extractURL(text) 30 | 31 | self.assertEqual(len(sourceList), 2) -------------------------------------------------------------------------------- /backend/grafit/management/commands/clear_neo4j.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from neomodel import db, clear_neo4j_database 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Clear the neo4j database' 8 | 9 | def handle(self, *args, **options): 10 | self.stdout.write('Deleting all nodes..\n') 11 | clear_neo4j_database(db) 12 | self.stdout.write('Done.\n') 13 | -------------------------------------------------------------------------------- /backend/grafit/management/commands/install_labels.py: -------------------------------------------------------------------------------- 1 | from django import setup as setup_django 2 | from django.core.management.base import BaseCommand 3 | 4 | from neomodel import install_all_labels 5 | 6 | 7 | class Command(BaseCommand): 8 | help = 'Install labels and constraints for your neo4j database' 9 | 10 | def handle(self, *args, **options): 11 | setup_django() 12 | install_all_labels(stdout=self.stdout) 13 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-25 09:36 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('auth', '0009_alter_user_last_name_max_length'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='User', 21 | fields=[ 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), 33 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 34 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 35 | ], 36 | options={ 37 | 'verbose_name': 'user', 38 | 'verbose_name_plural': 'users', 39 | 'abstract': False, 40 | }, 41 | managers=[ 42 | ('objects', django.contrib.auth.models.UserManager()), 43 | ], 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0002_article.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-27 09:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Article', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('title', models.CharField(max_length=250)), 18 | ('text', models.TextField()), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0004_article_related.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-29 12:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0003_load_data'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='article', 15 | name='related', 16 | field=models.ManyToManyField(related_name='_article_related_+', to='grafit.Article'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0005_auto_20181029_1255.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-29 12:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0004_article_related'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='article', 15 | name='related', 16 | field=models.ManyToManyField(blank=True, related_name='_article_related_+', to='grafit.Article'), 17 | ), 18 | migrations.AlterField( 19 | model_name='article', 20 | name='text', 21 | field=models.TextField(blank=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0006_fix_ids.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-25 09:36 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('grafit', '0005_auto_20181029_1255'), 14 | ] 15 | 16 | operations = [ 17 | migrations.RunSQL(""" 18 | BEGIN; 19 | SELECT setval(pg_get_serial_sequence('"grafit_article_related"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "grafit_article_related"; 20 | SELECT setval(pg_get_serial_sequence('"grafit_article"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "grafit_article"; 21 | COMMIT; 22 | """), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0007_auto_20181109_1633.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-09 16:33 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('grafit', '0006_fix_ids'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='article', 16 | name='created_at', 17 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 18 | preserve_default=False, 19 | ), 20 | migrations.AddField( 21 | model_name='article', 22 | name='updated_at', 23 | field=models.DateTimeField(auto_now=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0008_workspace.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-12 14:05 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('grafit', '0007_auto_20181109_1633'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Workspace', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=250)), 19 | ('initials', models.CharField(max_length=2)), 20 | ('users', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0009_article_workspace.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-14 13:13 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('grafit', '0008_workspace'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RunSQL(""" 15 | INSERT INTO grafit_workspace (id, name, initials) 16 | VALUES (1, 'Software Development', 'SW') 17 | ON CONFLICT DO NOTHING 18 | """), 19 | migrations.AddField( 20 | model_name='article', 21 | name='workspace', 22 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='grafit.Workspace'), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0010_fix_ids2.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-25 09:36 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('grafit', '0009_article_workspace'), 14 | ] 15 | 16 | operations = [ 17 | migrations.RunSQL(""" 18 | BEGIN; 19 | SELECT setval(pg_get_serial_sequence('"grafit_user_groups"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "grafit_user_groups"; 20 | SELECT setval(pg_get_serial_sequence('"grafit_user_user_permissions"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "grafit_user_user_permissions"; 21 | SELECT setval(pg_get_serial_sequence('"grafit_workspace_users"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "grafit_workspace_users"; 22 | SELECT setval(pg_get_serial_sequence('"grafit_workspace"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "grafit_workspace"; 23 | SELECT setval(pg_get_serial_sequence('"grafit_article_related"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "grafit_article_related"; 24 | SELECT setval(pg_get_serial_sequence('"grafit_article"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "grafit_article"; 25 | COMMIT; 26 | """), 27 | ] 28 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0011_remove_article_related.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-19 14:10 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0010_fix_ids2'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='article', 15 | name='related', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0012_create_search_index.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-21 18:15 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0011_remove_article_related'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RunSQL( 14 | """ 15 | DROP MATERIALIZED VIEW IF EXISTS grafit_search_index; 16 | 17 | CREATE MATERIALIZED VIEW grafit_search_index AS 18 | SELECT 19 | node.id, 20 | node.title, 21 | setweight(to_tsvector('english', node.title), 'A') || 22 | setweight(to_tsvector('english', node.text), 'B') as document 23 | FROM grafit_article AS node 24 | GROUP BY node.id; 25 | 26 | DROP INDEX IF EXISTS idx_grafit_search_index; 27 | 28 | CREATE INDEX idx_grafit_search_index 29 | ON grafit_search_index 30 | USING gin (document); 31 | """ 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0013_add_unique_to_searchindex.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-11-22 14:17 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0012_create_search_index'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RunSQL( 14 | """ 15 | DROP INDEX IF EXISTS grafit_search_unique; 16 | 17 | CREATE UNIQUE INDEX grafit_search_unique 18 | ON grafit_search_index (id); 19 | """ 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0014_create_search_word_index.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-12-05 17:10 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0013_add_unique_to_searchindex'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RunSQL( 14 | """ 15 | CREATE EXTENSION IF NOT EXISTS pg_trgm; 16 | DROP MATERIALIZED VIEW IF EXISTS grafit_search_word; 17 | 18 | CREATE MATERIALIZED VIEW grafit_search_word AS 19 | SELECT * 20 | FROM ts_stat( 21 | 'SELECT to_tsvector(''english'', node.title) || 22 | to_tsvector(''english'',coalesce(node.text, '''')) 23 | FROM grafit_article as node' 24 | ); 25 | 26 | CREATE INDEX words_idx ON grafit_search_word USING gin(word gin_trgm_ops); 27 | 28 | DROP INDEX IF EXISTS grafit_search_word_unique; 29 | CREATE UNIQUE INDEX grafit_search_word_unique ON grafit_search_word (word); 30 | """ 31 | ) 32 | ] 33 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0015_searchresult_searchword.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.3 on 2018-12-06 13:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0014_create_search_word_index'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='SearchResult', 15 | fields=[ 16 | ('id', models.BigIntegerField(primary_key=True, serialize=False)), 17 | ('title', models.TextField()), 18 | ('headline', models.TextField()), 19 | ('rank', models.DecimalField(decimal_places=2, max_digits=19)), 20 | ], 21 | options={ 22 | 'db_table': 'search_index', 23 | 'managed': False, 24 | }, 25 | ), 26 | migrations.CreateModel( 27 | name='SearchWord', 28 | fields=[ 29 | ('word', models.TextField(primary_key=True, serialize=False)), 30 | ('similarity', models.DecimalField(decimal_places=2, max_digits=19)), 31 | ], 32 | options={ 33 | 'db_table': 'search_word', 34 | 'managed': False, 35 | }, 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0016_add_webcontent_to_article.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-07 14:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0015_searchresult_searchword'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='article', 15 | name='update_web_content', 16 | field=models.BooleanField(default=True), 17 | ), 18 | migrations.AddField( 19 | model_name='article', 20 | name='web_content', 21 | field=models.TextField(default=None, null=True), 22 | ), 23 | migrations.RunSQL( 24 | """ 25 | DROP MATERIALIZED VIEW grafit_search_index; 26 | 27 | CREATE MATERIALIZED VIEW grafit_search_index AS 28 | SELECT 29 | node.id, 30 | node.title, 31 | setweight(to_tsvector('english', node.title), 'A') || 32 | setweight(to_tsvector('english', node.text), 'B') || 33 | setweight(to_tsvector('english', node.web_content), 'C') as document 34 | FROM grafit_article AS node 35 | GROUP BY node.id; 36 | 37 | DROP INDEX IF EXISTS idx_grafit_search_index; 38 | 39 | CREATE INDEX idx_grafit_search_index 40 | ON grafit_search_index 41 | USING gin (document); 42 | 43 | DROP INDEX IF EXISTS grafit_search_unique; 44 | 45 | CREATE UNIQUE INDEX grafit_search_unique 46 | ON grafit_search_index (id); 47 | """ 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /backend/grafit/migrations/0017_fix_null_search_index.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-09 14:54 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('grafit', '0016_add_webcontent_to_article'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RunSQL( 14 | """ 15 | DROP MATERIALIZED VIEW grafit_search_index; 16 | 17 | CREATE MATERIALIZED VIEW grafit_search_index AS 18 | SELECT 19 | node.id, 20 | node.title, 21 | setweight(to_tsvector('english', node.title), 'A') || 22 | setweight(to_tsvector('english', node.text), 'B') || 23 | setweight(to_tsvector('english', COALESCE(node.web_content, '')), 'C') as document 24 | FROM grafit_article AS node 25 | GROUP BY node.id; 26 | 27 | DROP INDEX IF EXISTS idx_grafit_search_index; 28 | 29 | CREATE INDEX idx_grafit_search_index 30 | ON grafit_search_index 31 | USING gin (document); 32 | 33 | DROP INDEX IF EXISTS grafit_search_unique; 34 | 35 | CREATE UNIQUE INDEX grafit_search_unique 36 | ON grafit_search_index (id); 37 | """ 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /backend/grafit/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/migrations/__init__.py -------------------------------------------------------------------------------- /backend/grafit/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import datetime 3 | 4 | from textblob import TextBlob 5 | 6 | from django.contrib.auth.models import AbstractUser 7 | from django.db import models 8 | from neomodel import BooleanProperty, StructuredNode, StringProperty, UniqueIdProperty, Relationship, StructuredRel, FloatProperty, DateTimeProperty 9 | from django.db.models import signals 10 | from django.dispatch import receiver 11 | from django.db import connection 12 | from .crawler.crawler import Crawler 13 | 14 | import logging 15 | 16 | 17 | logger = logging.getLogger() 18 | logger.setLevel(logging.INFO) 19 | logger.addHandler(logging.StreamHandler()) 20 | 21 | 22 | class User(AbstractUser): 23 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 24 | 25 | def __str__(self): 26 | return self.username 27 | 28 | 29 | class Workspace(models.Model): 30 | name = models.CharField(max_length=250) 31 | initials = models.CharField(max_length=2) 32 | users = models.ManyToManyField(User) 33 | 34 | 35 | class Article(models.Model): 36 | 37 | def __init__(self, *args, **kwargs): 38 | super(Article, self).__init__(*args, **kwargs) 39 | self.update_web_content = True 40 | 41 | title = models.CharField(max_length=250) 42 | text = models.TextField(blank=True) 43 | workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE) 44 | created_at = models.DateTimeField(auto_now_add=True) 45 | updated_at = models.DateTimeField(auto_now=True) 46 | web_content = models.TextField(null=True, default=None) 47 | update_web_content = models.BooleanField(default=True) 48 | 49 | @property 50 | def related(self): 51 | relatedNodes = [] 52 | try: 53 | ownNode = GraphArticle.nodes.get(uid=self.id) 54 | relatedGraphNodes = ownNode.related 55 | for node in relatedGraphNodes: 56 | rel = ownNode.related.relationship(node) 57 | if not rel.hidden: 58 | relatedNodes.append({ 59 | "id": int(node.uid), 60 | "title": node.name, 61 | "label": rel.label 62 | }) 63 | except: 64 | # article node does not exist yet 65 | pass 66 | 67 | return relatedNodes 68 | 69 | @property 70 | def shorttext(self): 71 | blob = TextBlob(self.text) 72 | firstSentences = " ".join([str(s) for s in blob.sentences[:3]]) 73 | if len(firstSentences) > 400: 74 | return self.text[:400].rsplit(' ', 1)[0] + " [...]" 75 | else: 76 | return firstSentences 77 | 78 | def __unicode__(self): 79 | return '{"title": %s, "title" %s}' % (self.id, self.title) 80 | 81 | 82 | @receiver([signals.post_save], sender=Article, dispatch_uid="crawl_url_in_article") 83 | def crawl_urls_in_article(sender, instance, created, **kwargs): 84 | if instance.update_web_content: 85 | crawler = Crawler() 86 | content = crawler.getWebContent(instance.text) 87 | Article.objects.filter(id=instance.id).update(web_content=content, update_web_content=False) 88 | 89 | 90 | @receiver([signals.post_save, signals.post_delete], sender=Article, dispatch_uid="update_search_index") 91 | def update_search_index(sender, instance, **kwargs): 92 | logger.info("update search index") 93 | cursor = connection.cursor() 94 | cursor.execute( 95 | 'REFRESH MATERIALIZED VIEW CONCURRENTLY grafit_search_index;') 96 | logger.info("finished updating search index") 97 | 98 | 99 | @receiver([signals.post_save, signals.post_delete], sender=Article, dispatch_uid="update_search_word") 100 | def update_search_word(sender, instance, **kwargs): 101 | logger.info("update search word") 102 | cursor = connection.cursor() 103 | cursor.execute( 104 | 'REFRESH MATERIALIZED VIEW CONCURRENTLY grafit_search_word;') 105 | logger.info("finished updating search word") 106 | 107 | 108 | class ArticleRel(StructuredRel): 109 | created_at = DateTimeProperty( 110 | default=lambda: datetime.now() 111 | ) 112 | tf_idf = FloatProperty() 113 | hidden = BooleanProperty(default=False) 114 | label = StringProperty() 115 | 116 | 117 | class GraphArticle(StructuredNode): 118 | uid = UniqueIdProperty() 119 | name = StringProperty() 120 | related = Relationship('GraphArticle', 'RELATED', model=ArticleRel) 121 | 122 | 123 | class SearchResult(models.Model): 124 | id = models.BigIntegerField(primary_key=True) 125 | title = models.TextField() 126 | headline = models.TextField() 127 | rank = models.DecimalField(max_digits=19, decimal_places=2) 128 | 129 | class Meta: 130 | managed = False 131 | db_table = 'search_index' 132 | 133 | 134 | class SearchWord(models.Model): 135 | word = models.TextField(primary_key=True) 136 | similarity = models.DecimalField(max_digits=19, decimal_places=2) 137 | 138 | class Meta: 139 | managed = False 140 | db_table = 'search_word' 141 | -------------------------------------------------------------------------------- /backend/grafit/search/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/search/__init__.py -------------------------------------------------------------------------------- /backend/grafit/search/search.py: -------------------------------------------------------------------------------- 1 | from ..models import SearchResult, SearchWord 2 | import logging 3 | 4 | logger = logging.getLogger() 5 | logger.setLevel(logging.INFO) 6 | 7 | 8 | class Search: 9 | 10 | @staticmethod 11 | def __cleanSearchTerm(searchTerm: str) -> str: 12 | 13 | searchTerm = searchTerm.strip() 14 | 15 | # Remove critical character for ts_query 16 | searchTerm = searchTerm.replace("&", " ") 17 | searchTerm = searchTerm.replace("!", " ") 18 | searchTerm = searchTerm.replace("|", " ") 19 | 20 | # Remove multiple withspace 21 | searchTerm = ' '.join(searchTerm.split()) 22 | 23 | # Add OR between all words 24 | searchTerm = searchTerm.replace(" ", "|") 25 | 26 | return searchTerm 27 | 28 | @staticmethod 29 | def __runSearchQuery(searchTerm: str): 30 | return SearchResult.objects.raw(''' 31 | SELECT grafit_search_index.id, ts_headline(grafit_search_index.title, to_tsquery('english', %(query)s)) as title, ts_headline(grafit_article.text, to_tsquery('english', %(query)s)) as headline, ts_rank(document, to_tsquery('english', %(query)s)) as rank 32 | FROM grafit_search_index 33 | INNER JOIN grafit_article ON grafit_article.id = grafit_search_index.id 34 | WHERE grafit_search_index.document @@ to_tsquery('english', %(query)s) 35 | ORDER BY rank DESC 36 | LIMIT 25''', {'query': searchTerm}) 37 | 38 | @staticmethod 39 | def __findSimilarWord(word: str) -> str: 40 | 41 | queryset = SearchWord.objects.raw(''' 42 | SELECT word, similarity(word, %(word)s) as similarity 43 | FROM grafit_search_word 44 | WHERE similarity(word, %(word)s) > 0.5 45 | ORDER BY similarity desc 46 | LIMIT 1; 47 | ''', {'word': word}) 48 | 49 | if len(queryset) > 0: 50 | for item in queryset: 51 | return item.word 52 | return None 53 | 54 | @staticmethod 55 | def search(searchTerm: str): 56 | cleanSearchTerm = Search.__cleanSearchTerm(searchTerm) 57 | logger.info("searching for: " + cleanSearchTerm) 58 | 59 | queryset = Search.__runSearchQuery(cleanSearchTerm) 60 | resultsCount = len(queryset) 61 | 62 | if(resultsCount < 1): 63 | searchWords = searchTerm.split("|") 64 | for word in searchWords: 65 | similarWord = Search.__findSimilarWord(word) 66 | if similarWord is not None: 67 | cleanSearchTerm += '|' 68 | cleanSearchTerm += similarWord 69 | queryset = Search.__runSearchQuery(cleanSearchTerm) 70 | 71 | return queryset -------------------------------------------------------------------------------- /backend/grafit/search/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/search/tests/__init__.py -------------------------------------------------------------------------------- /backend/grafit/search/tests/test_search.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from rest_framework.test import force_authenticate, APITestCase, APIClient 3 | from rest_framework import status 4 | from django.urls import reverse 5 | from grafit.search.search import Search 6 | from grafit.models import Article, Workspace, User 7 | 8 | 9 | class SearchTest(TestCase): 10 | def test_cleanSearchTerm_spaceToOr(self): 11 | testTerm = "word1 word2" 12 | expectedResult = "word1|word2" 13 | assert Search._Search__cleanSearchTerm(testTerm) == expectedResult 14 | 15 | def test_cleanSearchTerm_removeAnd(self): 16 | testTerm = "word1&word2" 17 | expectedResult = "word1|word2" 18 | assert Search._Search__cleanSearchTerm(testTerm) == expectedResult 19 | 20 | def test_cleanSearchTerm_removeExcamation(self): 21 | testTerm = "word1&word2" 22 | expectedResult = "word1|word2" 23 | assert Search._Search__cleanSearchTerm(testTerm) == expectedResult 24 | 25 | def test_cleanSearchTerm_removeMultispace(self): 26 | testTerm = " word1 word2 " 27 | expectedResult = "word1|word2" 28 | assert Search._Search__cleanSearchTerm(testTerm) == expectedResult 29 | 30 | 31 | class SearchAPITest(APITestCase): 32 | def setUp(self): 33 | test_user = User.objects.create_user( 34 | 'testuser2', 'test@example.com', 'testpassword') 35 | self.workspace = Workspace(name="Testworkspace2", initials="TE") 36 | self.workspace.save() 37 | self.workspace.users.add(test_user) 38 | 39 | for i in range(0, 50): 40 | Article.objects.create( 41 | title="TestTitle", text="TestText", workspace=self.workspace) 42 | 43 | self.client = APIClient() 44 | self.user = User.objects.get(username='testuser2') 45 | self.client.force_authenticate(user=self.user) 46 | 47 | def test_search_limit(self): 48 | response = self.client.get( 49 | reverse('search-list'), {'searchTerm': 'TestTitle'}, 50 | format="json") 51 | 52 | self.assertEqual(response.status_code, status.HTTP_200_OK) 53 | self.assertEqual(len(response.data), min( 54 | Article.objects.filter(workspace__users=self.user).count(), 25)) 55 | 56 | def test_search_fuzzy(self): 57 | testArticle = Article.objects.create(title="Open Source Document Database MongoDB", 58 | text="We're the creators of MongoDB, the most popular database for modern apps, and MongoDB Atlas, the global cloud database on AWS, Azure, and GCP. Easily organize, use, and enrich data — in real time, anywhere.", workspace=self.workspace) 59 | 60 | response = self.client.get( 61 | reverse('search-list'), {'searchTerm': 'MongoD'}, 62 | format="json") 63 | 64 | results = [] 65 | 66 | for respnseObject in response.data: 67 | results.append(respnseObject['id']) 68 | 69 | self.assertEqual(response.status_code, status.HTTP_200_OK) 70 | self.assertIn(testArticle.id, results) 71 | -------------------------------------------------------------------------------- /backend/grafit/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import Group 2 | from rest_framework import serializers 3 | from rest_framework.validators import UniqueTogetherValidator 4 | from .concept_runner import ConceptRunner 5 | 6 | from .models import User, Article, Workspace, GraphArticle, SearchResult, SearchWord 7 | 8 | 9 | class UserSerializer(serializers.HyperlinkedModelSerializer): 10 | class Meta: 11 | model = User 12 | fields = ('url', 'username', 'email', 13 | 'groups', 'first_name', 'last_name') 14 | 15 | 16 | class CreateUserSerializer(serializers.ModelSerializer): 17 | def create(self, validated_data): 18 | # call create_user on user object. Without this 19 | # the password will be stored in plain text. 20 | user = User.objects.create_user(**validated_data) 21 | return user 22 | 23 | class Meta: 24 | model = User 25 | fields = ('url', 'username', 'password', 26 | 'first_name', 'last_name', 'email',) 27 | extra_kwargs = {'password': {'write_only': True}} 28 | 29 | 30 | class GroupSerializer(serializers.HyperlinkedModelSerializer): 31 | class Meta: 32 | model = Group 33 | fields = ('url', 'name') 34 | 35 | 36 | class WorkspaceSerializer(serializers.ModelSerializer): 37 | class Meta: 38 | model = Workspace 39 | fields = ('id', 'url', 'name', 'initials') 40 | 41 | 42 | class SearchResultSerializer(serializers.ModelSerializer): 43 | class Meta: 44 | model = SearchResult 45 | fields = ('id', 'title', 'headline', 'rank') 46 | 47 | 48 | class SearchWordSerializer(serializers.ModelSerializer): 49 | class Meta: 50 | model = SearchWord 51 | fields = ('word', 'similarity') 52 | 53 | 54 | class ArticleTitleSerializer(serializers.ModelSerializer): 55 | class Meta: 56 | model = Article 57 | fields = ('id', 'url', 'title', 'workspace') 58 | 59 | 60 | class ArticleSerializer(serializers.ModelSerializer): 61 | class Meta: 62 | model = Article 63 | fields = ('id', 'url', 'title', 'text', 'shorttext', 64 | 'related', 'workspace', 'created_at', 'updated_at') 65 | 66 | validators = [ 67 | UniqueTogetherValidator( 68 | queryset=Article.objects.all(), 69 | fields=('title', 'workspace') 70 | ) 71 | ] 72 | 73 | def _save_related(self, article): 74 | ConceptRunner.generate_concepts_for_article(article.id) 75 | 76 | def create(self, validated_data): 77 | article = Article.objects.create(**validated_data) 78 | self._save_related(article) 79 | return article 80 | 81 | def update(self, instance, validated_data): 82 | instance.update_web_content = True 83 | super(ArticleSerializer, self).update(instance, validated_data) 84 | self._save_related(instance) 85 | return instance 86 | -------------------------------------------------------------------------------- /backend/grafit/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/backend/grafit/tests/__init__.py -------------------------------------------------------------------------------- /backend/grafit/tests/test_articles.py: -------------------------------------------------------------------------------- 1 | from rest_framework.test import force_authenticate, APITestCase, APIClient 2 | from django.urls import reverse 3 | from rest_framework import status 4 | 5 | from ..models import Article, Workspace, User 6 | 7 | 8 | class ArticleTest(APITestCase): 9 | def setUp(self): 10 | test_user = User.objects.create_user('testuser', 'test@example.com', 'testpassword') 11 | workspace = Workspace(name="Testworkspace", initials="TE") 12 | workspace.save() 13 | workspace.users.add(test_user) 14 | 15 | Article.objects.create(title="TestTitle", text="TestText", workspace=workspace) 16 | 17 | self.client = APIClient() 18 | self.user = User.objects.get(username='testuser') 19 | self.client.force_authenticate(user=self.user) 20 | self.workspace = workspace 21 | 22 | def test_article_list(self): 23 | raw_response = self.client.get( 24 | reverse('article-list'), 25 | format="json") 26 | response = raw_response.data['results'] 27 | 28 | self.assertEqual(raw_response.status_code, status.HTTP_200_OK) 29 | self.assertEqual(len(response), Article.objects.filter(workspace__users=self.user).count()) 30 | self.assertEqual(response[0]['title'], "TestTitle") 31 | self.assertEqual(response[0]['text'], "TestText") 32 | 33 | def test_article_list_pagination(self): 34 | 35 | for i in range(0, 200): 36 | Article.objects.create(title="TestTitle", text="TestText", workspace=self.workspace) 37 | 38 | raw_response = self.client.get( 39 | reverse('article-list'), 40 | format="json") 41 | response = raw_response.data['results'] 42 | 43 | self.assertEqual(raw_response.status_code, status.HTTP_200_OK) 44 | self.assertEqual(len(response), min(Article.objects.filter(workspace__users=self.user).count(), 25)) 45 | self.assertEqual(response[0]['title'], "TestTitle") 46 | self.assertEqual(response[0]['text'], "TestText") 47 | 48 | def test_article_list_workspace_permission(self): 49 | other_user = User.objects.create_user('test2', 'test2@example.com', 'testpassword') 50 | other_workspace = Workspace(name="Testworkspace", initials="TE") 51 | other_workspace.save() 52 | other_workspace.users.add(other_user) 53 | 54 | Article.objects.create(title="TestTitle2", text="TestText2", workspace=other_workspace) 55 | 56 | raw_response = self.client.get( 57 | reverse('article-list'), 58 | format="json") 59 | response = raw_response.data['results'] 60 | 61 | self.assertEqual(raw_response.status_code, status.HTTP_200_OK) 62 | self.assertEqual(len(response), Article.objects.filter(workspace__users=self.user).count()) 63 | 64 | def test_article_create(self): 65 | url = reverse('article-list') 66 | data = { 67 | 'title': 'New Article Title', 68 | 'text': 'Test test test test', 69 | 'workspace': Workspace.objects.get(name="Testworkspace").id 70 | } 71 | response = self.client.post(url, data, format='json') 72 | 73 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 74 | self.assertEqual(Article.objects.get(title='New Article Title').title, 'New Article Title') 75 | self.assertEqual(Article.objects.get(title='New Article Title').text, 'Test test test test') 76 | 77 | def test_article_create_notext(self): 78 | numberOfArticles = Article.objects.all().count() 79 | 80 | url = reverse('article-list') 81 | data = { 82 | 'title': 'New Article Title', 83 | 'workspace': Workspace.objects.get(name="Testworkspace").id 84 | } 85 | response = self.client.post(url, data, format='json') 86 | 87 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 88 | self.assertEqual(Article.objects.count(), numberOfArticles + 1) 89 | self.assertEqual(Article.objects.get(title="New Article Title").title, 'New Article Title') 90 | 91 | def test_update_article(self): 92 | numberOfArticles = Article.objects.all().count() 93 | testArticleId = Article.objects.get(title="TestTitle").id 94 | 95 | data = { 96 | 'title': 'Test123', 97 | 'text': '', 98 | 'workspace': Workspace.objects.get(name="Testworkspace").id 99 | } 100 | response = self.client.put(reverse('article-detail', args=[testArticleId]), data, format="json") 101 | 102 | self.assertEqual(response.status_code, status.HTTP_200_OK) 103 | self.assertEqual(Article.objects.count(), numberOfArticles) 104 | self.assertEqual(Article.objects.get(pk=testArticleId).title, 'Test123') 105 | self.assertEqual(Article.objects.get(pk=testArticleId).text, '') 106 | 107 | def test_article_delete(self): 108 | numberOfArticles = Article.objects.all().count() 109 | articleId = Article.objects.get(title="TestTitle").id 110 | response = self.client.delete(reverse('article-detail', args=[articleId])) 111 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 112 | self.assertEqual(Article.objects.all().count(), numberOfArticles - 1) 113 | -------------------------------------------------------------------------------- /backend/grafit/tests/test_workspaces.py: -------------------------------------------------------------------------------- 1 | from rest_framework.test import force_authenticate, APITestCase, APIClient 2 | from django.urls import reverse 3 | from rest_framework import status 4 | 5 | from ..models import Workspace, User 6 | 7 | 8 | class WorkspaceTest(APITestCase): 9 | def setUp(self): 10 | testWorkspace = Workspace(name="Testworkspace", 11 | initials="TE") 12 | testWorkspace.save() 13 | 14 | User.objects.create_user( 15 | 'testuser', 'test@example.com', 'testpassword') 16 | 17 | testWorkspace.users.add(User.objects.get(username='testuser')) 18 | Workspace.objects.create(name="fail", 19 | initials="FA") 20 | 21 | self.client = APIClient() 22 | self.user = User.objects.get(username='testuser') 23 | self.client.force_authenticate(user=self.user) 24 | 25 | def test_workspace_get(self): 26 | """ 27 | Create two workspaces. Add one to the testuser. Check if only one of them is returned. 28 | """ 29 | 30 | response = self.client.get( 31 | reverse('workspace-list'), 32 | format="json") 33 | 34 | self.assertEqual(len(response.data), 1) 35 | self.assertEqual(response.data[0]['initials'], 'TE') 36 | 37 | def test_workspace_create(self): 38 | 39 | numberOfWorkspaces = Workspace.objects.all().count() 40 | 41 | data = { 42 | "name": "Test Workspace", 43 | "initials": "TW" 44 | } 45 | 46 | response = self.client.post( 47 | reverse('workspace-list'), data, format="json") 48 | 49 | self.assertEqual(Workspace.objects.count(), numberOfWorkspaces + 1) 50 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 51 | 52 | def test_workspace_create_noinitials(self): 53 | data = { 54 | "name": "Test Workspace", 55 | "initials": "" 56 | } 57 | 58 | response = self.client.post( 59 | reverse('workspace-list'), data, format="json") 60 | 61 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 62 | 63 | def test_workspace_create_notitle(self): 64 | data = { 65 | "name": "", 66 | "initials": "TW" 67 | } 68 | 69 | response = self.client.post( 70 | reverse('workspace-list'), data, format="json") 71 | 72 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 73 | 74 | def test_workspace_create_autoasign(self): 75 | 76 | originalWorkspaceCount = len(self.client.get( 77 | reverse('workspace-list'), 78 | format="json").data) 79 | 80 | data = { 81 | "name": "Test Workspace", 82 | "initials": "TW" 83 | } 84 | 85 | response = self.client.post( 86 | reverse('workspace-list'), data, format="json") 87 | 88 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 89 | 90 | newWorkspaceCount = len(self.client.get( 91 | reverse('workspace-list'), 92 | format="json").data) 93 | 94 | self.assertEqual(newWorkspaceCount, originalWorkspaceCount + 1) 95 | -------------------------------------------------------------------------------- /backend/grafit/views.py: -------------------------------------------------------------------------------- 1 | import time 2 | from django.contrib.auth.models import Group 3 | from .serializers import UserSerializer, GroupSerializer, ArticleTitleSerializer, CreateUserSerializer, ArticleSerializer, WorkspaceSerializer, SearchResultSerializer 4 | from rest_framework import viewsets, mixins, status 5 | from rest_framework.permissions import AllowAny 6 | from rest_framework.pagination import LimitOffsetPagination 7 | from rest_framework.response import Response 8 | from rest_framework.views import APIView 9 | from .concept_runner import ConceptRunner 10 | from .search.search import Search 11 | from .models import User, Article, Workspace, GraphArticle 12 | import logging 13 | 14 | 15 | logger = logging.getLogger() 16 | logger.setLevel(logging.INFO) 17 | logger.addHandler(logging.StreamHandler()) 18 | 19 | 20 | class UserViewSet(mixins.RetrieveModelMixin, 21 | mixins.UpdateModelMixin, 22 | viewsets.GenericViewSet): 23 | """ 24 | API endpoint that allows users to be viewed or edited. 25 | """ 26 | queryset = User.objects.all().order_by('-date_joined') 27 | serializer_class = UserSerializer 28 | 29 | 30 | class UserCreateViewSet(mixins.CreateModelMixin, 31 | viewsets.GenericViewSet): 32 | """ 33 | Creates user accounts 34 | """ 35 | queryset = User.objects.all() 36 | serializer_class = CreateUserSerializer 37 | permission_classes = (AllowAny,) 38 | 39 | 40 | class GroupViewSet(viewsets.ModelViewSet): 41 | """ 42 | API endpoint that allows groups to be viewed or edited. 43 | """ 44 | queryset = Group.objects.all() 45 | serializer_class = GroupSerializer 46 | 47 | 48 | class ArticleTitleViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): 49 | serializer_class = ArticleTitleSerializer 50 | 51 | def get_queryset(self): 52 | user = self.request.user 53 | return Article.objects.filter(workspace__users=user) 54 | 55 | 56 | class ArticleViewSet(viewsets.ModelViewSet): 57 | serializer_class = ArticleSerializer 58 | pagination_class = LimitOffsetPagination 59 | 60 | def get_queryset(self): 61 | user = self.request.user 62 | return Article.objects.filter(workspace__users=user) 63 | 64 | def list(self, request): 65 | articles = self.filter_queryset(Article.objects.filter( 66 | workspace__users=request.user).exclude(text__exact="").order_by('-updated_at')) 67 | page = self.paginate_queryset(articles) 68 | serializer = self.get_serializer(page, many=True) 69 | return self.get_paginated_response(serializer.data) 70 | 71 | 72 | class WorkspaceViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.ListModelMixin, 73 | viewsets.GenericViewSet): 74 | serializer_class = WorkspaceSerializer 75 | 76 | def perform_create(self, serializer): 77 | serializer.save(users=[self.request.user]) 78 | 79 | def get_queryset(self): 80 | """ 81 | This view should return a list of all the workspaces 82 | for the currently authenticated user. 83 | """ 84 | user = self.request.user 85 | return Workspace.objects.filter(users=user) 86 | 87 | 88 | class SearchResultViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): 89 | queryset = [] 90 | 91 | def list(self, request): 92 | searchTerm = request.query_params.get('searchTerm', None) 93 | 94 | if not searchTerm: 95 | return Response([]) 96 | 97 | queryset = Search.search(searchTerm) 98 | 99 | serializer = SearchResultSerializer(queryset, many=True) 100 | return Response(serializer.data) 101 | 102 | 103 | class ConceptRunnerAPI(APIView): 104 | def get(self, request, format=None): 105 | articleId = request.query_params.get('id') 106 | try: 107 | if articleId: 108 | ConceptRunner.generate_concepts_for_article(int(articleId)) 109 | else: 110 | ConceptRunner.generate_graph() 111 | return Response(status.HTTP_200_OK) 112 | except: 113 | return Response(status.HTTP_500_INTERNAL_SERVER_ERROR) 114 | 115 | 116 | class AddConceptAPI(APIView): 117 | def post(self, request): 118 | if "from" in request.data and "to" in request.data: 119 | fromId = request.data["from"] 120 | toId = request.data["to"] 121 | 122 | label = "unlabeled" 123 | if "label" in request.data: 124 | label = request.data["label"] 125 | 126 | try: 127 | article_node_from = GraphArticle.nodes.get_or_none(uid=fromId) 128 | article_node_to = GraphArticle.nodes.get_or_none(uid=toId) 129 | 130 | if not article_node_from or not article_node_to: 131 | return Response(status.HTTP_404_NOT_FOUND) 132 | else: 133 | # TODO add by user flag to connection 134 | rel = article_node_from.related.relationship(article_node_to) 135 | if rel: 136 | rel.hidden = False 137 | rel.label = label 138 | rel.save() 139 | else: 140 | article_node_from.related.connect( 141 | article_node_to, {'tf_idf': 0, 'label': label}) 142 | return Response(status.HTTP_201_CREATED) 143 | 144 | except: 145 | return Response(status.HTTP_500_INTERNAL_SERVER_ERROR) 146 | 147 | else: 148 | return Response(status.HTTP_404_NOT_FOUND) 149 | 150 | 151 | class HideConceptAPI(APIView): 152 | def post(self, request): 153 | 154 | if "from" in request.data and "to" in request.data: 155 | fromId = request.data["from"] 156 | toId = request.data["to"] 157 | 158 | try: 159 | article_node_from = GraphArticle.nodes.get_or_none(uid=fromId) 160 | article_node_to = GraphArticle.nodes.get_or_none(uid=toId) 161 | 162 | if not article_node_from or not article_node_to: 163 | return Response(status.HTTP_404_NOT_FOUND) 164 | else: 165 | rel = article_node_from.related.relationship(article_node_to) 166 | rel.hidden = True 167 | rel.save() 168 | return Response(rel.hidden) 169 | 170 | except: 171 | return Response(status.HTTP_500_INTERNAL_SERVER_ERROR) 172 | 173 | else: 174 | return Response(status.HTTP_404_NOT_FOUND) 175 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /backend/wait_for_neo4j.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from time import time, sleep 4 | 5 | from neomodel import db 6 | 7 | DATABASE_URL = 'bolt://neo4j:test@neo4j:7687' 8 | 9 | check_timeout = os.getenv("NEO4J_CHECK_TIMEOUT", 60) 10 | check_interval = os.getenv("NEO4J_CHECK_INTERVAL", 1) 11 | interval_unit = "second" if check_interval == 1 else "seconds" 12 | 13 | start_time = time() 14 | logger = logging.getLogger() 15 | logger.setLevel(logging.INFO) 16 | logger.addHandler(logging.StreamHandler()) 17 | 18 | 19 | def n4j_isready(): 20 | while time() - start_time < check_timeout: 21 | try: 22 | db.set_connection(DATABASE_URL) 23 | logger.info("Neo4J is ready!") 24 | return True 25 | except: 26 | logger.info(f"Neo4j isn't ready. Waiting for {check_interval} {interval_unit}...") 27 | sleep(check_interval) 28 | 29 | logger.error(f"We could not connect to Neo4j within {check_timeout} seconds.") 30 | return False 31 | 32 | 33 | n4j_isready() 34 | -------------------------------------------------------------------------------- /backend/wait_for_postgres.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from time import time, sleep 4 | 5 | import psycopg2 6 | 7 | check_timeout = os.getenv("POSTGRES_CHECK_TIMEOUT", 30) 8 | check_interval = os.getenv("POSTGRES_CHECK_INTERVAL", 1) 9 | interval_unit = "second" if check_interval == 1 else "seconds" 10 | config = { 11 | "dbname": os.getenv("POSTGRES_DB", "postgres"), 12 | "user": os.getenv("POSTGRES_USER", "postgres"), 13 | "password": os.getenv("POSTGRES_PASSWORD", ""), 14 | "host": os.getenv("DATABASE_URL", "db") 15 | } 16 | 17 | start_time = time() 18 | logger = logging.getLogger() 19 | logger.setLevel(logging.INFO) 20 | logger.addHandler(logging.StreamHandler()) 21 | 22 | 23 | def pg_isready(host, user, password, dbname): 24 | while time() - start_time < check_timeout: 25 | try: 26 | conn = psycopg2.connect(**vars()) 27 | logger.info("Postgres is ready!") 28 | conn.close() 29 | return True 30 | except psycopg2.OperationalError: 31 | logger.info(f"Postgres isn't ready. Waiting for {check_interval} {interval_unit}...") 32 | sleep(check_interval) 33 | 34 | logger.error(f"We could not connect to Postgres within {check_timeout} seconds.") 35 | return False 36 | 37 | 38 | pg_isready(**config) 39 | -------------------------------------------------------------------------------- /docker-compose.build.yml: -------------------------------------------------------------------------------- 1 | version: "2.3" 2 | 3 | services: 4 | db: 5 | image: postgres 6 | 7 | neo4j: 8 | image: neo4j 9 | environment: 10 | NEO4J_AUTH: "neo4j/test" 11 | 12 | backend: 13 | image: grafit-backend 14 | build: 15 | context: ./backend 16 | target: prod 17 | ports: 18 | - 8000:8000 19 | restart: always 20 | volumes: 21 | - ./backend:/code 22 | depends_on: 23 | - db 24 | - neo4j 25 | 26 | frontend: 27 | image: grafit-frontend 28 | build: 29 | context: ./frontend 30 | target: prod 31 | ports: 32 | - 3000:80 33 | restart: always 34 | volumes: 35 | - ./frontend:/usr/src/grafit 36 | - /usr/src/grafit/node_modules 37 | depends_on: 38 | - backend 39 | 40 | documentation: 41 | image: grafit-docs 42 | build: ./mkdocs 43 | restart: always 44 | volumes: 45 | - ./mkdocs:/code 46 | ports: 47 | - "8001:8001" 48 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "2.3" 2 | 3 | services: 4 | db: 5 | image: postgres 6 | 7 | neo4j: 8 | image: neo4j 9 | ports: 10 | - 7474:7474 11 | - 7687:7687 12 | environment: 13 | NEO4J_AUTH: "neo4j/test" 14 | 15 | backend: 16 | image: grafit-backend 17 | build: 18 | context: ./backend 19 | target: base 20 | ports: 21 | - 8000:8000 22 | restart: always 23 | volumes: 24 | - ./backend:/code 25 | depends_on: 26 | - db 27 | - neo4j 28 | 29 | frontend: 30 | image: grafit-frontend 31 | build: 32 | context: ./frontend 33 | target: base 34 | ports: 35 | - 3000:3000 36 | restart: always 37 | volumes: 38 | - ./frontend:/usr/src/grafit 39 | - /usr/src/grafit/node_modules 40 | depends_on: 41 | - backend 42 | 43 | documentation: 44 | restart: always 45 | build: ./mkdocs 46 | volumes: 47 | - ./mkdocs:/code 48 | ports: 49 | - "8001:8001" 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.3" 2 | 3 | services: 4 | db: 5 | image: postgres 6 | 7 | neo4j: 8 | image: neo4j 9 | environment: 10 | NEO4J_AUTH: "neo4j/test" 11 | 12 | backend: 13 | image: grafitio/grafit-backend 14 | ports: 15 | - 8000:8000 16 | restart: always 17 | depends_on: 18 | - db 19 | - neo4j 20 | 21 | frontend: 22 | image: grafitio/grafit-frontend 23 | ports: 24 | - 3000:80 25 | restart: always 26 | depends_on: 27 | - backend 28 | 29 | documentation: 30 | image: grafitio/grafit-docs 31 | restart: always 32 | ports: 33 | - "8001:8001" 34 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # swap files 24 | .swp 25 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.12.0-alpine as base 2 | 3 | LABEL Name=grafit-frontend-dev Version=0.0.1 4 | EXPOSE 3000 5 | 6 | COPY . /usr/src/grafit 7 | WORKDIR /usr/src/grafit 8 | 9 | # If you are building your code for production 10 | # RUN npm install --only=production 11 | RUN npm install 12 | CMD [ "npm", "start" ] 13 | 14 | 15 | FROM base as build 16 | RUN npm run build 17 | 18 | 19 | FROM nginx:1.15.5-alpine as prod 20 | 21 | # remove default confs 22 | RUN rm /etc/nginx/conf.d/default.conf 23 | 24 | LABEL Name=grafit-frontend Version=0.0.1 25 | EXPOSE 80 26 | 27 | COPY --from=build /usr/src/grafit/build /usr/share/nginx/html 28 | COPY ./nginx.conf /etc/nginx/nginx.conf 29 | 30 | # test nginx config 31 | RUN nginx -t -------------------------------------------------------------------------------- /frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | #tcp_nopush on; 25 | 26 | keepalive_timeout 65; 27 | 28 | #gzip on; 29 | 30 | server { 31 | listen 80; 32 | server_name localhost; 33 | 34 | location / { 35 | root /usr/share/nginx/html; 36 | try_files $uri $uri/ /index.html; 37 | } 38 | 39 | # redirect server error pages to the static page /50x.html 40 | error_page 500 502 503 504 /50x.html; 41 | location = /50x.html { 42 | root /usr/share/nginx/html; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "bootstrap": "^4.1.3", 7 | "node-sass": "^4.11.0", 8 | "react": "^16.5.2", 9 | "react-bootstrap": "^0.32.4", 10 | "react-dom": "^16.5.2", 11 | "react-router-dom": "^4.3.1", 12 | "react-scripts": "2.0.5", 13 | "react-tag-input": "^6.1.0", 14 | "react-treebeard": "^3.1.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafit-io/grafit/2823fec3905d7c9cde48cd787224bcc3b7454c0f/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 14 | 18 | 22 | 23 | 27 | 28 | 37 |{article.shorttext}
70 |{this.state.article.text}
177 |14 | 21 |
22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/constants.js: -------------------------------------------------------------------------------- 1 | let BASE_URL = window.location.origin.toString() + "/"; 2 | 3 | if ( 4 | window.location.hostname === "localhost" || 5 | window.location.hostname === "127.0.0.1" || 6 | window.location.hostname === "0.0.0.0" 7 | ) { 8 | BASE_URL = "http://" + window.location.hostname + ":8000/"; 9 | } 10 | 11 | export const AUTH_API = BASE_URL + "api-token-auth/"; 12 | export const API = BASE_URL + "api/v1/"; 13 | 14 | export const JWT_LOCALSTOR_KEY = "jwt"; 15 | 16 | export const ARTICLE_ENDPOINT = "articles/"; 17 | export const ARTICLETITLE_ENDPOINT = "articletitless"; 18 | export const USER_ENDPOINT = "users/"; 19 | export const WORKSPACE_ENDPOINT = "workspaces/"; 20 | export const SEARCH_ENDPOINT = "search/?searchTerm="; 21 | export const ADDRELATED_ENDPOINT = "addconcept"; 22 | export const HIDE_ENDPOINT = "hideconcept"; 23 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | margin: 0; 4 | padding: 0; 5 | font-family: "Quicksand", -apple-system, BlinkMacSystemFont, "Segoe UI", 6 | "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 7 | "Helvetica Neue", sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | height: 100%; 11 | } 12 | 13 | #root, 14 | .row-offcanvas { 15 | height: 100%; 16 | } 17 | 18 | .content { 19 | border-top:6px solid var(--primary-400); 20 | height: 100%; 21 | overflow: auto; 22 | padding: 50px; 23 | } 24 | 25 | code { 26 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 27 | monospace; 28 | } 29 | 30 | .btn-primary { 31 | background-color: var(--primary-700); 32 | color: var(--grey-100); 33 | } 34 | .btn-primary:hover, 35 | .btn-primary:focus, 36 | .btn-primary:active, 37 | .btn-primary.visited, 38 | .open > .dropdown-toggle.btn-primary { 39 | background-color: var(--primary-400); 40 | color: #ffffff; 41 | border-color: var(--primary-300); 42 | } 43 | 44 | .btn-primary.disabled, 45 | .btn-primary[disabled], 46 | fieldset[disabled] .btn-primary { 47 | background-color: var(--primary-800); 48 | } 49 | 50 | .navbar-inverse { 51 | background-color: var(--grey-900); 52 | font-weight: 900; 53 | } 54 | 55 | :root { 56 | --primary-000: hsl(258, 30%, 98%); 57 | --primary-100: hsl(258, 30%, 84%); 58 | --primary-200: hsl(258, 30%, 76%); 59 | --primary-300: hsl(258, 30%, 68%); 60 | --primary-400: hsl(258, 30%, 60%); 61 | --primary-500: hsl(258, 30%, 52%); 62 | --primary-600: hsl(258, 30%, 44%); 63 | --primary-700: hsl(258, 30%, 36%); 64 | --primary-800: hsl(258, 30%, 28%); 65 | --primary-900: hsl(258, 30%, 20%); 66 | 67 | --grey-000: hsl(0, 0%, 98%); 68 | --grey-100: hsl(0, 0%, 93%); 69 | --grey-200: hsl(0, 0%, 83%); 70 | --grey-300: hsl(0, 0%, 73%); 71 | --grey-400: hsl(0, 0%, 63%); 72 | --grey-500: hsl(0, 0%, 53%); 73 | --grey-600: hsl(0, 0%, 43%); 74 | --grey-700: hsl(0, 0%, 33%); 75 | --grey-800: hsl(0, 0%, 23%); 76 | --grey-900: hsl(0, 0%, 13%); 77 | } 78 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(