├── .gitignore ├── .travis.yml ├── .travis └── prepare.sh ├── README.md ├── app.py ├── crm ├── __init__.py ├── apps │ ├── __init__.py │ ├── address │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── admin │ │ ├── __init__.py │ │ ├── config.py │ │ ├── converters.py │ │ ├── formatters.py │ │ ├── mixins.py │ │ ├── models.py │ │ ├── templates │ │ │ └── admin │ │ │ │ ├── extra.html │ │ │ │ ├── index.html │ │ │ │ └── model │ │ │ │ ├── create.html │ │ │ │ ├── details.html │ │ │ │ ├── edit.html │ │ │ │ ├── inline_form.html │ │ │ │ ├── inline_list_base.html │ │ │ │ ├── inner_field_list.html │ │ │ │ ├── list.html │ │ │ │ └── row_actions.html │ │ └── views.py │ ├── alert │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── api │ │ ├── __init__.py │ │ ├── models.py │ │ └── views.py │ ├── comment │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── company │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── contact │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── country │ │ ├── __init__.py │ │ ├── countries.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── currency │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── deal │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── email │ │ ├── __init__.py │ │ └── models.py │ ├── event │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── home │ │ ├── __init__.py │ │ ├── models.py │ │ ├── templates │ │ │ └── home │ │ │ │ ├── 401.html │ │ │ │ └── index.html │ │ └── views.py │ ├── image │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── knowledge │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── link │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── message │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── organization │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── passport │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── phone │ │ ├── __init__.py │ │ └── models.py │ ├── project │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── reports │ │ ├── templates │ │ │ └── reports │ │ │ │ └── reports.html │ │ └── views.py │ ├── sprint │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── tag │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ ├── task │ │ ├── __init__.py │ │ ├── graphql │ │ │ ├── __init__.py │ │ │ ├── arguments.py │ │ │ ├── mutations.py │ │ │ ├── queries.py │ │ │ └── types.py │ │ └── models.py │ └── user │ │ ├── __init__.py │ │ ├── graphql │ │ ├── __init__.py │ │ ├── arguments.py │ │ ├── mutations.py │ │ ├── queries.py │ │ └── types.py │ │ ├── models.py │ │ └── tasks.py ├── cli │ ├── __init__.py │ ├── createdb.py │ ├── dumpcache.py │ ├── dumpdata.py │ ├── generate_graphql_docs.py │ ├── import_data.py │ ├── load.py │ ├── loaddata.py │ ├── loadfixtures.py │ ├── mailer.py │ ├── rq_worker.py │ ├── syncdata.py │ └── update_currency.py ├── db.py ├── events │ ├── __init__.py │ ├── cache_db_updates.py │ ├── catch_db_updates.py │ ├── notify_new_task.py │ ├── send_email_on_new_message.py │ └── update_auto_fields.py ├── fixtures.py ├── graphql.py ├── mailer.py ├── middlewares │ ├── __init__.py │ └── iyo.py ├── rq.py ├── settings.py ├── settings_dev.py ├── settings_prod.py └── settings_test.py ├── docs ├── AddNewRequirement.md ├── AdminInterface.md ├── AuthenticationMiddleware.md ├── Caching.md ├── Commands.md ├── Configuration.md ├── DBEvents.md ├── GraphqlAdvanced.md ├── GraphqlHTTPClient.md ├── GraphqlOverview.md ├── GraphqlQueriesAndMutations.md ├── GraphqlQueryLanguage.md ├── HowToCreateAppplication.md ├── Installation.md ├── LoadDumpData.md ├── MailinMailout.md ├── Middlewares.md ├── Migrations.md ├── Models.md ├── Philosophy.md ├── PrefabInstallation.md ├── Production.md ├── Running.md ├── Structure.md ├── Views.md ├── assets │ ├── addfilter.png │ ├── createview.png │ ├── detailsview.png │ ├── editview.png │ ├── extramenu.png │ ├── graphiql_docs.png │ ├── graphql_many.png │ ├── graphql_query.png │ ├── iyo-settings1.png │ ├── iyo-settings2.png │ ├── listview.png │ └── mainmenu.png └── graphqlapi │ ├── addressarguments.doc.html │ ├── addresstype.doc.html │ ├── assets │ ├── code.css │ └── require-by.css │ ├── boolean.doc.html │ ├── comment.doc.html │ ├── commentarguments.doc.html │ ├── commenttypeconnection.doc.html │ ├── commenttypeedge.doc.html │ ├── companyarguments.doc.html │ ├── companytags.doc.html │ ├── contact.doc.html │ ├── contactarguments.doc.html │ ├── contactsubgrouparguments.doc.html │ ├── contacttypeconnection.doc.html │ ├── contacttypeedge.doc.html │ ├── countriesenum.doc.html │ ├── countryarguments.doc.html │ ├── createcontactarguments.doc.html │ ├── createcontacts.doc.html │ ├── createdealarguments.doc.html │ ├── createdeals.doc.html │ ├── currencyarguments.doc.html │ ├── datetime.doc.html │ ├── deal.doc.html │ ├── dealarguments.doc.html │ ├── dealstate.doc.html │ ├── dealtype.doc.html │ ├── dealtypeconnection.doc.html │ ├── dealtypeedge.doc.html │ ├── deletecontacts.doc.html │ ├── deletedeals.doc.html │ ├── deprecated.doc.html │ ├── directive.spec.html │ ├── directivelocation.spec.html │ ├── enumvalue.spec.html │ ├── event.doc.html │ ├── eventarguments.doc.html │ ├── eventtypeconnection.doc.html │ ├── eventtypeedge.doc.html │ ├── field.spec.html │ ├── float.doc.html │ ├── fonts │ └── webfonts │ │ ├── SalesforceSans-Bold.eot │ │ ├── SalesforceSans-Bold.svg │ │ ├── SalesforceSans-Bold.woff │ │ ├── SalesforceSans-Bold.woff2 │ │ ├── SalesforceSans-BoldItalic.eot │ │ ├── SalesforceSans-BoldItalic.svg │ │ ├── SalesforceSans-BoldItalic.woff │ │ ├── SalesforceSans-BoldItalic.woff2 │ │ ├── SalesforceSans-Italic.eot │ │ ├── SalesforceSans-Italic.svg │ │ ├── SalesforceSans-Italic.woff │ │ ├── SalesforceSans-Italic.woff2 │ │ ├── SalesforceSans-Light.eot │ │ ├── SalesforceSans-Light.svg │ │ ├── SalesforceSans-Light.woff │ │ ├── SalesforceSans-Light.woff2 │ │ ├── SalesforceSans-LightItalic.eot │ │ ├── SalesforceSans-LightItalic.svg │ │ ├── SalesforceSans-LightItalic.woff │ │ ├── SalesforceSans-LightItalic.woff2 │ │ ├── SalesforceSans-Regular.eot │ │ ├── SalesforceSans-Regular.svg │ │ ├── SalesforceSans-Regular.woff │ │ ├── SalesforceSans-Regular.woff2 │ │ ├── SalesforceSans-Thin.eot │ │ ├── SalesforceSans-Thin.svg │ │ ├── SalesforceSans-Thin.woff │ │ ├── SalesforceSans-Thin.woff2 │ │ ├── SalesforceSans-ThinItalic.eot │ │ ├── SalesforceSans-ThinItalic.svg │ │ ├── SalesforceSans-ThinItalic.woff │ │ └── SalesforceSans-ThinItalic.woff2 │ ├── gender.doc.html │ ├── id.doc.html │ ├── image.doc.html │ ├── imagetypeconnection.doc.html │ ├── imagetypeedge.doc.html │ ├── include.doc.html │ ├── index.html │ ├── inputvalue.spec.html │ ├── int.doc.html │ ├── linkarguments.doc.html │ ├── linktype.doc.html │ ├── message.doc.html │ ├── messagearguments.doc.html │ ├── messagetypeconnection.doc.html │ ├── messagetypeedge.doc.html │ ├── mutations.doc.html │ ├── node.doc.html │ ├── organization.doc.html │ ├── organizationarguments.doc.html │ ├── organizationtypeconnection.doc.html │ ├── organizationtypeedge.doc.html │ ├── pageinfo.doc.html │ ├── passportarguments.doc.html │ ├── passporttype.doc.html │ ├── project.doc.html │ ├── projectarguments.doc.html │ ├── projecttypeconnection.doc.html │ ├── projecttypeedge.doc.html │ ├── query.doc.html │ ├── schema.spec.html │ ├── scripts │ ├── filter-types.js │ ├── focus-active.js │ └── toggle-navigation.js │ ├── skip.doc.html │ ├── sprint.doc.html │ ├── sprintarguments.doc.html │ ├── sprinttypeconnection.doc.html │ ├── sprinttypeedge.doc.html │ ├── string.doc.html │ ├── styles │ ├── _custom.scss │ ├── _override.scss │ ├── graphdoc.css │ └── graphdoc.scss │ ├── tagarguments.doc.html │ ├── task.doc.html │ ├── taskarguments.doc.html │ ├── tasktypeconnection.doc.html │ ├── tasktypeedge.doc.html │ ├── type.spec.html │ ├── typekind.spec.html │ ├── updatecontactarguments.doc.html │ ├── updatecontacts.doc.html │ ├── updatedealarguments.doc.html │ ├── updatedeals.doc.html │ ├── user.doc.html │ ├── userarguments.doc.html │ ├── usertypeconnection.doc.html │ └── usertypeedge.doc.html ├── frontend ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── README.md ├── build │ ├── build.js │ ├── check-versions.js │ ├── dev-client.js │ ├── dev-server.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ ├── webpack.prod.conf.js │ └── webpack.test.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ ├── prod.env.js │ └── test.env.js ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── axioshelpers.js │ ├── components │ │ ├── Deal.vue │ │ ├── DealsList.vue │ │ ├── DealsPending.vue │ │ ├── Home.vue │ │ └── Reports.vue │ ├── main.js │ └── router │ │ └── index.js └── test │ └── unit │ ├── .eslintrc │ ├── index.js │ ├── karma.conf.js │ └── specs │ └── Hello.spec.js ├── prepare.sh ├── requirements-dev.apt ├── requirements-testing.pip ├── requirements.apt ├── requirements.brew ├── requirements.npm ├── requirements.pip ├── run_tests.sh ├── setup.py ├── specs ├── AlertManagement.md ├── Assetmanagement.md ├── Beta2Test.md ├── MailinMailout.md ├── Redis.md ├── Requirements.md └── beta2_basic_test_usecases.md ├── static └── js │ ├── apollo.min.js │ ├── chart.min.js │ ├── vue-apollo.min.js │ ├── vue-chart.min.js │ └── vue.min.js ├── tests ├── __init__.py ├── base_tests.py └── test_home.py ├── utils └── google_spreadsheet_example.gs └── uwsgi.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | ./build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | db/ 104 | 105 | #pycharm 106 | .idea 107 | 108 | # data dir 109 | data 110 | 111 | # db 112 | *.db 113 | 114 | # migrations 115 | migrations 116 | 117 | # static 118 | static 119 | 120 | # graphql schema 121 | schema.graphql 122 | 123 | # tar.gz 124 | *.gz 125 | *.zip 126 | 127 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: python 4 | python: 5 | - "3.5" 6 | install: .travis/prepare.sh 7 | script: ./run_tests.sh 8 | 9 | 10 | addons: 11 | apt: 12 | packages: 13 | - python3-dev 14 | - libffi-dev 15 | - build-essential 16 | 17 | notification: 18 | email: 19 | recipients: 20 | - husseina@greenitglobe.com 21 | on_success: never 22 | on_failure: always -------------------------------------------------------------------------------- /.travis/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | export LC_ALL="en_US.UTF-8" 5 | export LC_CTYPE="en_US.UTF-8" 6 | pip install -r requirements-testing.pip 7 | pip install -e . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## CRM 3 | 4 | product owner: @hamdy_farag (backup xmonader) 5 | 6 | **Philosophy** 7 | - [Why we need our own CRM](docs/Philosophy.md) 8 | 9 | **Basics** 10 | - [Installation](docs/Installation.md) 11 | - [Running](docs/Running.md) 12 | - [Configuration](docs/Configuration.md) 13 | - [Project structure](docs/Structure.md) 14 | - [Authentication](docs/AuthenticationMiddleware.md) 15 | - [Caching](docs/Caching.md) 16 | - [How to define a new SQLALCHEMY model](docs/Models.md) 17 | - [How to add a new required dependency](docs/AddNewRequirement.md) 18 | - [How to develop a new typical application in the CRM](docs/HowToCreateAppplication.md) 19 | 20 | **Advanced** 21 | - [Admin Interface](docs/AdminInterface.md) 22 | - [DB Migrations](docs/Migrations.md) 23 | - [Add custom command](docs/Commands.md) 24 | - [Add middleware](docs/Middlewares.md) 25 | - [Add new SqlAlchemy DB event](docs/DBEvents.md) 26 | - [Load & Dump Data Algorithms](docs/LoadDumpData.md) 27 | - [Mail in/out](docs/MailinMailout.md) 28 | 29 | **Graphql** 30 | - [Intro](docs/GraphqlOverview.md) 31 | - [CRM API General overview](docs/GraphqlQueriesAndMutations.md) 32 | - [Graphql API Docs](http://htmlpreview.github.io/?https://github.com/Incubaid/crm/blob/master/docs/graphqlapi/index.html) 33 | - [GraphQl API Query language](docs/GraphqlQueryLanguage.md) 34 | - [Using HTTP client to access CRM Graphql API](docs/GraphqlHTTPClient.md) 35 | - [Define new Types, Queries, and Mutations](docs/GraphqlAdvanced.md) 36 | 37 | **Deployment To production** 38 | 39 | - [Procedures to update production](docs/Production.md) 40 | - [Install CRM from Prefab/Jumpscale](docs/PrefabInstallation.md) 41 | 42 | **Specs** 43 | - [Requirements](specs/Requirements.md) 44 | - [Alert Management](specs/AlertManagement.md) 45 | - [Asset Management](specs/Assetmanagement.md) 46 | - [Implementation](specs/Implementation.md) 47 | - [Redis](specs/Redis.md) 48 | - [Beta2 Test](specs/Beta2Test.md) 49 | - [Mailin/Mailout](specs/MailinMailout.md) 50 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from crm import app 4 | from crm.cli import * 5 | from crm.middlewares import * 6 | 7 | 8 | if 'loaddata' not in sys.argv and 'loadfixtures' not in sys.argv: 9 | import crm.events 10 | -------------------------------------------------------------------------------- /crm/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/__init__.py -------------------------------------------------------------------------------- /crm/apps/address/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/address/__init__.py -------------------------------------------------------------------------------- /crm/apps/address/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/address/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/address/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | from crm.apps.country.graphql.arguments import CountryArguments 5 | from crm.graphql import BaseArgument 6 | 7 | 8 | class AddressArguments(InputObjectType, BaseArgument): 9 | """ 10 | Address Arguments 11 | """ 12 | street_number = graphene.String() 13 | street_name = graphene.String() 14 | city = graphene.String() 15 | state = graphene.String() 16 | country = graphene.Argument(CountryArguments) 17 | zip_code = graphene.String() 18 | 19 | # **************************************************** 20 | # NO NEED FOR CREATE/UPDATE ADDRESS * 21 | # THIS IS DONE IMPLICITLY THROUGH CONTACT & DEAL APIs * 22 | # ***************************************************** 23 | -------------------------------------------------------------------------------- /crm/apps/address/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/address/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/address/graphql/queries.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | 3 | from crm.apps.address.graphql.arguments import AddressArguments 4 | from crm.apps.address.graphql.types import AddressType 5 | from crm.graphql import BaseQuery, CRMConnectionField 6 | 7 | 8 | class AddressQuery(BaseQuery): 9 | addresses = CRMConnectionField( 10 | AddressType, 11 | **AddressArguments.fields() 12 | ) 13 | 14 | class Meta: 15 | interfaces = (relay.Node,) 16 | -------------------------------------------------------------------------------- /crm/apps/address/graphql/types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from graphene import relay 4 | 5 | from graphene_sqlalchemy import SQLAlchemyObjectType 6 | 7 | from crm.apps.address.models import Address 8 | from crm.apps.country.graphql.types import CountryType 9 | from crm.graphql import CrmType 10 | 11 | 12 | class AddressType(CrmType, SQLAlchemyObjectType): 13 | country = graphene.Field(CountryType) 14 | 15 | class Meta: 16 | model = Address 17 | interfaces = (relay.Node,) 18 | name = model.__name__ 19 | 20 | # CrmType adds author_original, author_last objects rather than ids 21 | exclude_fields = ( 22 | 'author_original_id', 23 | 'author_last_id' 24 | ) -------------------------------------------------------------------------------- /crm/apps/address/models.py: -------------------------------------------------------------------------------- 1 | from crm.db import db, BaseModel 2 | 3 | 4 | class Address(db.Model, BaseModel): 5 | 6 | __tablename__ = "addresses" 7 | 8 | street_number = db.Column( 9 | db.String(255), 10 | index=True 11 | ) 12 | 13 | street_name = db.Column( 14 | db.String(255), 15 | default="", 16 | index=True 17 | ) 18 | 19 | description = db.Column( 20 | db.Text(), 21 | default="", 22 | index=True 23 | ) 24 | 25 | city = db.Column( 26 | db.Text(), 27 | default="", 28 | index=True 29 | ) 30 | 31 | state = db.Column( 32 | db.Text(), 33 | default="", 34 | index=True 35 | ) 36 | 37 | zip_code = db.Column( 38 | db.String(255), 39 | index=True 40 | ) 41 | 42 | country_id = db.Column( 43 | db.String(5), 44 | db.ForeignKey('countries.id') 45 | ) 46 | 47 | contact_id = db.Column( 48 | db.String(5), 49 | db.ForeignKey('contacts.id') 50 | ) 51 | 52 | company_id = db.Column( 53 | db.String(5), 54 | db.ForeignKey('companies.id') 55 | ) 56 | 57 | deal_id = db.Column( 58 | db.String(5), 59 | db.ForeignKey('deals.id') 60 | ) 61 | 62 | @property 63 | def formatted_address(self): 64 | address = '' 65 | if self.street_name: 66 | if self.street_number: 67 | address += '%s %s, ' % (self.street_number, self.street_name) 68 | else: 69 | address += '%s, ' % self.street_name 70 | 71 | if self.state: 72 | address += '%s, ' % self.state 73 | if self.city: 74 | address += '%s, ' % self.city 75 | if self.country: 76 | address += str(self.country.name) 77 | 78 | if self.zip_code: 79 | address += ' (zip code: %s)' % self.zip_code 80 | 81 | return address 82 | 83 | def __str__(self): 84 | return self.formatted_address 85 | -------------------------------------------------------------------------------- /crm/apps/admin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/admin/__init__.py -------------------------------------------------------------------------------- /crm/apps/admin/config.py: -------------------------------------------------------------------------------- 1 | NAV_BAR_ORDER = { 2 | 'MAIN': ["User", "Company", "Contact", "Organization", "Deal", "Project", "Sprint", "Task", "Event", "Currency"], 3 | 'EXTRA': ["Link", "Comment", "Message", "Tag", "KnowledgeBaseCategory", "KnowledgeBase"], 4 | } 5 | -------------------------------------------------------------------------------- /crm/apps/admin/converters.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy 2 | import flask_admin 3 | from flask_admin.form.fields import Select2Field 4 | from flask_admin.model.form import converts 5 | 6 | class EnumField(Select2Field): 7 | def __init__(self, column, **kwargs): 8 | assert isinstance(column.type, sqlalchemy.sql.sqltypes.Enum) 9 | 10 | def coercer(value): 11 | # coerce incoming value into an enum value 12 | if isinstance(value, column.type.enum_class): 13 | return value 14 | elif isinstance(value, str): 15 | return column.type.enum_class[value] 16 | else: 17 | assert False 18 | 19 | super(EnumField, self).__init__( 20 | choices=[(v, v) for v in column.type.enums], 21 | coerce=coercer, 22 | **kwargs) 23 | 24 | def pre_validate(self, form): 25 | # we need to override the default SelectField validation because it 26 | # apparently tries to directly compare the field value with the choice 27 | # key; it is not clear how that could ever work in cases where the 28 | # values and choice keys must be different types 29 | 30 | for (v, _) in self.choices: 31 | if self.data == self.coerce(v): 32 | break 33 | else: 34 | raise ValueError(self.gettext('Not a valid choice')) 35 | 36 | 37 | class CustomAdminConverter(flask_admin.contrib.sqla.form.AdminModelConverter): 38 | @converts("sqlalchemy.sql.sqltypes.Enum") 39 | def conv_enum(self, field_args, **extra): 40 | return EnumField(column=extra["column"], **field_args) 41 | 42 | -------------------------------------------------------------------------------- /crm/apps/admin/mixins.py: -------------------------------------------------------------------------------- 1 | """ 2 | ADMIN INTERFACE RELATED LOGIC 3 | """ 4 | 5 | 6 | class AdminLinksMixin(object): 7 | ADMIN_EDIT_LINK = "/{modelname}/edit/?id={modelid}" 8 | ADMIN_LIST_LINK = "/{modelname}/" 9 | ADMIN_VIEW_LINK = "/{modelname}/details/?id={modelid}" 10 | 11 | ADMIN_CREATE_LINK = "/{modelname}/new/?id={modelid}" 12 | 13 | ADMIN_EDIT_LINK_MODAL = "/{modelname}/edit/?id={modelid}" # &modal=True" 14 | ADMIN_VIEW_LINK_MODAL = "/{modelname}/details/?id={modelid}" 15 | ADMIN_CREATE_LINK_MODAL = "/{modelname}/new/?url=/{modelname}" 16 | 17 | def _format_link(self, link): 18 | d = { 19 | 'modelname': self.__class__.__name__.lower() 20 | } 21 | 22 | if '{modelid}' in link: 23 | d['modelid'] = self.id 24 | 25 | return link.format(**d) 26 | 27 | def admin_list_link(self): 28 | return self._format_link(AdminLinksMixin.ADMIN_LIST_LINK) 29 | 30 | def admin_edit_link(self): 31 | return self._format_link(AdminLinksMixin.ADMIN_EDIT_LINK) 32 | 33 | def admin_view_link(self): 34 | return self._format_link(AdminLinksMixin.ADMIN_VIEW_LINK) 35 | 36 | def admin_create_link(self): 37 | return self._format_link(AdminLinksMixin.ADMIN_CREATE_LINK) 38 | 39 | def admin_edit_link_modal(self): 40 | return self._format_link(AdminLinksMixin.ADMIN_EDIT_LINK_MODAL) 41 | 42 | def admin_view_link_modal(self): 43 | return self._format_link(AdminLinksMixin.ADMIN_VIEW_LINK_MODAL) 44 | 45 | def admin_create_link_modal(self): 46 | return self._format_link(AdminLinksMixin.ADMIN_CREATE_LINK_MODAL) 47 | -------------------------------------------------------------------------------- /crm/apps/admin/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/admin/models.py -------------------------------------------------------------------------------- /crm/apps/admin/templates/admin/extra.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/static.html' as admin_static with context %} 2 | {% import 'admin/model/layout.html' as model_layout with context %} 3 | 4 | 5 | {# ---------------------- Forms -------------------------- #} 6 | {% macro render_tabular_field(form, field, kwargs={}, caller=None) %} 7 | 8 | 9 | 10 | {% set direct_error = h.is_field_error(field.errors) %} 11 |
12 | 13 | 20 |
21 | {% set _dummy = kwargs.setdefault('class', 'form-control') %} 22 | {{ field(**kwargs)|safe }} 23 | 24 |
25 | {% if caller %} 26 | {{ caller(form, field, direct_error, kwargs) }} 27 | {% endif %} 28 |
29 | 30 | {% endmacro %} 31 | 32 | {% macro render_tabular_form_fields(form, form_opts=None) %} 33 | {% if form.hidden_tag is defined %} 34 | {{ form.hidden_tag() }} 35 | {% else %} 36 | {% if csrf_token %} 37 | 38 | {% endif %} 39 | {% for f in form if f.widget.input_type == 'hidden' %} 40 | {{ f }} 41 | {% endfor %} 42 | {% endif %} 43 | 44 | {% if form_opts and form_opts.form_rules %} 45 | {% for r in form_opts.form_rules %} 46 | {{ r(form, form_opts=form_opts) }} 47 | {% endfor %} 48 | {% else %} 49 | {% for f in form if f.widget.input_type != 'hidden' %} 50 | {% set kwargs = {} %} 51 | {% if form_opts %} 52 | {% set kwargs = form_opts.widget_args.get(f.short_name, {}) %} 53 | {% endif %} 54 | {% if form._obj %} 55 | {% do kwargs.update({'pk': form._obj.id}) %} 56 | {% do kwargs.update({'obj': form._obj}) %} 57 | {% endif %} 58 | {{ render_tabular_field(form, f, kwargs) }} 59 | {% endfor %} 60 | {% endif %} 61 | {% endmacro %} 62 | 63 | -------------------------------------------------------------------------------- /crm/apps/admin/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | 3 | {% block body %} 4 | 5 |

Quick links

6 | 7 |
8 | 9 | 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /crm/apps/admin/templates/admin/model/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | {% from 'admin/lib.html' import extra with context %} {# backward compatible #} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | {{ lib.form_css() }} 8 | {% endblock %} 9 | 10 | {% block body %} 11 | {% block navlinks %} 12 | 20 | {% endblock %} 21 | 22 | {% block create_form %} 23 | {{ lib.render_form(form, return_url, {}, form_opts) }} 24 | {% endblock %} 25 | {% endblock %} 26 | 27 | {% block tail %} 28 | {{ super() }} 29 | {{ lib.form_js() }} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /crm/apps/admin/templates/admin/model/details.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | 4 | {% block body %} 5 | {% block navlinks %} 6 | 24 | {% endblock %} 25 | 26 | {% block details_search %} 27 |
28 | {{ _gettext('Filter') }} 29 | 30 |
31 | {% endblock %} 32 | 33 | {% block details_table %} 34 | 35 | 36 | 37 | {% for c, name in details_columns %} 38 | {% set attr = getattr(model, c) %} 39 | {% if attr or (hasattr(attr, 'length') and attr|length > 0) %} 40 | 41 | 50 | 51 | 58 | 59 | {% endif %} 60 | {% endfor %} 61 |
42 | {% set potentialviewname = (c+"view").lower()%} 43 | {% if potentialviewname in g._admin_view._template_args['filtered_objects'] %} 44 | {% set tofilterview, filtername = g._admin_view._template_args['filtered_objects'][potentialviewname]%} 45 | {{name}} 46 | {% else %} 47 | {{ name }} 48 | {% endif %} 49 | 52 | {% if hasattr(attr, "admin_view_link") %} 53 | {{get_value(model, c)}} 54 | {% else %} 55 | {{ get_value(model, c) }} 56 | {% endif %} 57 |
62 | {% endblock %} 63 | {% endblock %} 64 | 65 | {% block tail %} 66 | {{ super() }} 67 | 68 | {% endblock %} 69 | -------------------------------------------------------------------------------- /crm/apps/admin/templates/admin/model/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% import 'admin/lib.html' as lib with context %} 3 | {% from 'admin/lib.html' import extra with context %} {# backward compatible #} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | {{ lib.form_css() }} 8 | {% endblock %} 9 | 10 | {% block body %} 11 | {% block navlinks %} 12 | 30 | {% endblock %} 31 | 32 | {% block edit_form %} 33 | {{ lib.render_form(form, return_url, {}, form_opts) }} 34 | {% endblock %} 35 | {% endblock %} 36 | 37 | {% block tail %} 38 | {{ super() }} 39 | {{ lib.form_js() }} 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /crm/apps/admin/templates/admin/model/inline_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% import 'admin/lib.html' as lib with context %} 4 | {% import 'admin/extra.html' as extralib with context %} 5 | 6 |
7 | 8 | {{ extralib.render_tabular_form_fields(field.form, form_opts=form_opts) }} 9 |
10 | -------------------------------------------------------------------------------- /crm/apps/admin/templates/admin/model/inner_field_list.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/model/inline_list_base.html' as base with context %} 2 | 3 | {% macro render_field(field) %} 4 | {{ field }} 5 | 6 | {% if h.is_field_error(field.errors) %} 7 | 12 | {% endif %} 13 | {% endmacro %} 14 | 15 | {{ base.render_inline_fields(field, template, render_field, check) }} 16 | -------------------------------------------------------------------------------- /crm/apps/admin/templates/admin/model/row_actions.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/lib.html' as lib with context %} 2 | 3 | 4 | {% macro link(action, url, icon_class=None) %} 5 | 6 | 7 | 8 | {% endmacro %} 9 | 10 | {% macro view_row(action, row_id, row) %} 11 | {{ link(action, get_url('.details_view', id=row_id), 'fa fa-eye glyphicon glyphicon-eye-open') }} 12 | {% endmacro %} 13 | 14 | {% macro view_row_popup(action, row_id, row) %} 15 | {{ lib.add_modal_button(url=get_url('.details_view', id=row_id, url=return_url, modal=True), title=action.title, content='') }} 16 | {% endmacro %} 17 | 18 | {% macro edit_row(action, row_id, row) %} 19 | {{ link(action, get_url('.edit_view', id=row_id), 'fa fa-pencil glyphicon glyphicon-pencil') }} 20 | {% endmacro %} 21 | 22 | {% macro edit_row_popup(action, row_id, row) %} 23 | {{ lib.add_modal_button(url=get_url('.edit_view', id=row_id, url=return_url, modal=True), title=action.title, content='') }} 24 | {% endmacro %} 25 | 26 | {% macro delete_row(action, row_id, row) %} 27 |
28 | {{ delete_form.id(value=get_pk_value(row)) }} 29 | {{ delete_form.url(value=return_url) }} 30 | {% if delete_form.csrf_token %} 31 | {{ delete_form.csrf_token }} 32 | {% elif csrf_token %} 33 | 34 | {% endif %} 35 | 38 |
39 | {% endmacro %} 40 | 41 | 42 | {% macro view_row(action, row_id, row) %} 43 | {{ link(action, get_url('.details_view', id=row_id), 'fa fa-eye glyphicon glyphicon-eye-open') }} 44 | {% endmacro %} 45 | 46 | 47 | {% macro edit_row(action, row_id, row) %} 48 | {{ link(action, get_url('.edit_view', id=row_id), 'fa fa-pencil glyphicon glyphicon-pencil') }} 49 | {% endmacro %} 50 | -------------------------------------------------------------------------------- /crm/apps/alert/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/alert/__init__.py -------------------------------------------------------------------------------- /crm/apps/alert/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/alert/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/alert/graphql/arguments.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/alert/graphql/arguments.py -------------------------------------------------------------------------------- /crm/apps/alert/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/alert/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/alert/graphql/queries.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/alert/graphql/queries.py -------------------------------------------------------------------------------- /crm/apps/alert/graphql/types.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/alert/graphql/types.py -------------------------------------------------------------------------------- /crm/apps/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/api/__init__.py -------------------------------------------------------------------------------- /crm/apps/api/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/api/models.py -------------------------------------------------------------------------------- /crm/apps/api/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify 2 | 3 | from flask_graphql import GraphQLView 4 | 5 | from crm import app 6 | 7 | 8 | @app.route('/api', methods=["POST"]) 9 | def api(): 10 | if request.headers.get('Content-Type', '').lower() != 'application/json': 11 | return jsonify(errors=['Only accepts Content-Type: application/json']), 400 12 | 13 | data = request.json 14 | query = data.get('query', None) 15 | if not query: 16 | return jsonify(errors=['query field is missing']), 400 17 | if query: 18 | try: 19 | execresult = app.graphql_schema.execute(query) 20 | if execresult.errors: 21 | # BAD REQUEST ON ERRORS 22 | return jsonify(errors=[str(e) for e in execresult.errors]), 400 23 | result = list(execresult.data.items())[0][1] 24 | if result is None: 25 | return '', 404 26 | return jsonify(execresult.data), 200 27 | 28 | except Exception as ex: 29 | return jsonify(errors=[str(ex)]), 400 30 | 31 | 32 | app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=app.graphql_schema, graphiql=True)) 33 | -------------------------------------------------------------------------------- /crm/apps/comment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/comment/__init__.py -------------------------------------------------------------------------------- /crm/apps/comment/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/comment/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/comment/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | 5 | class CommentArguments(InputObjectType): 6 | uid = graphene.String() 7 | content = graphene.String() 8 | company = graphene.Field('crm.apps.company.graphql.arguments.CompanyArguments') 9 | contact = graphene.Field('crm.apps.contact.graphql.arguments.ContactArguments') 10 | user = graphene.Field('crm.apps.user.graphql.arguments.UserArguments') 11 | deal = graphene.Field('crm.apps.deal.graphql.arguments.DealArguments') 12 | task = graphene.Field('crm.apps.task.graphql.arguments.TaskArguments') 13 | organization = graphene.Field('crm.apps.organization.graphql.arguments.OrganizationArguments') 14 | project = graphene.Field('crm.apps.project.graphql.arguments.ProjectArguments') 15 | sprint = graphene.Field('crm.apps.sprint.graphql.arguments.SprintArguments') 16 | link = graphene.Field('crm.apps.link.graphql.arguments.LinkArguments') 17 | event = graphene.Field('crm.apps.event.graphql.arguments.EventArguments') 18 | 19 | 20 | class CreateCommentArguments(CommentArguments): 21 | content = graphene.String(required=True) 22 | 23 | 24 | class UpdateCommentArguments(CommentArguments): 25 | uid = graphene.String(required=True) 26 | -------------------------------------------------------------------------------- /crm/apps/comment/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/comment/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/comment/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import CommentType 4 | from crm.graphql import BaseQuery 5 | 6 | 7 | class CommentQuery(BaseQuery): 8 | comments = graphene.List(CommentType) 9 | 10 | def resolve_comments(self, args, context, info): 11 | query = CommentType.get_query(context) 12 | return query.all() 13 | -------------------------------------------------------------------------------- /crm/apps/comment/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | from graphene_sqlalchemy import SQLAlchemyObjectType 3 | 4 | from crm.apps.comment.models import Comment 5 | 6 | 7 | class CommentType(SQLAlchemyObjectType): 8 | 9 | class Meta: 10 | model = Comment 11 | interfaces = (relay.Node,) 12 | name = model.__name__ 13 | -------------------------------------------------------------------------------- /crm/apps/comment/models.py: -------------------------------------------------------------------------------- 1 | from crm.db import db, BaseModel 2 | 3 | 4 | class Comment(db.Model, BaseModel): 5 | 6 | __tablename__ = "comments" 7 | 8 | # should be markdown. 9 | content = db.Column( 10 | db.Text(), 11 | index=True 12 | ) 13 | 14 | company_id = db.Column( 15 | db.String(5), 16 | db.ForeignKey("companies.id") 17 | ) 18 | 19 | contact_id = db.Column( 20 | db.String(5), 21 | db.ForeignKey("contacts.id") 22 | ) 23 | 24 | user_id = db.Column( 25 | db.String(5), 26 | db.ForeignKey("users.id") 27 | ) 28 | 29 | deal_id = db.Column( 30 | db.String(5), 31 | db.ForeignKey("deals.id") 32 | ) 33 | 34 | task_id = db.Column( 35 | db.String(5), 36 | db.ForeignKey("tasks.id") 37 | ) 38 | 39 | organization_id = db.Column( 40 | db.String(5), 41 | db.ForeignKey("organizations.id") 42 | ) 43 | 44 | project_id = db.Column( 45 | db.String(5), 46 | db.ForeignKey("projects.id") 47 | ) 48 | 49 | sprint_id = db.Column( 50 | db.String(5), 51 | db.ForeignKey("sprints.id") 52 | ) 53 | 54 | link_id = db.Column( 55 | db.String(5), 56 | db.ForeignKey("links.id") 57 | ) 58 | event_id = db.Column( 59 | db.String, 60 | db.ForeignKey("events.id") 61 | ) 62 | 63 | # alert_id = db.Column( 64 | # db.String, 65 | # db.ForeignKey("alerts.id") 66 | # ) 67 | # 68 | # alert_source_id = db.Column( 69 | # db.String, 70 | # db.ForeignKey("alertsources.id") 71 | # ) 72 | 73 | knowledge_base_id = db.Column( 74 | db.String, 75 | db.ForeignKey("knowledgebases.id") 76 | ) 77 | 78 | def __str__(self): 79 | return self.content 80 | -------------------------------------------------------------------------------- /crm/apps/company/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/company/__init__.py -------------------------------------------------------------------------------- /crm/apps/company/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/company/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/company/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | from crm.apps.address.graphql.arguments import AddressArguments 5 | from crm.apps.comment.graphql.arguments import CommentArguments 6 | from crm.apps.contact.graphql.arguments import ContactArguments 7 | from crm.apps.deal.graphql.arguments import DealArguments 8 | from crm.apps.link.graphql.arguments import LinkArguments 9 | from crm.apps.message.graphql.arguments import MessageArguments 10 | from crm.apps.tag.graphql.arguments import TagArguments 11 | from crm.apps.task.graphql.arguments import TaskArguments 12 | 13 | 14 | class CompanyArguments(InputObjectType): 15 | uid = graphene.String() 16 | name = graphene.String() 17 | description = graphene.String() 18 | vatnumber = graphene.String() 19 | website = graphene.String() 20 | emails = graphene.String() 21 | telephones = graphene.String() 22 | owner = graphene.Argument('crm.apps.user.graphql.arguments.UserArguments') 23 | ownerbackup = graphene.Argument('crm.apps.user.graphql.arguments.UserArguments') 24 | 25 | deals = graphene.List(DealArguments) 26 | comments = graphene.List(CommentArguments) 27 | tasks = graphene.List(TaskArguments) 28 | messages = graphene.List(MessageArguments) 29 | links = graphene.List(LinkArguments) 30 | contacts = graphene.List(ContactArguments) 31 | 32 | tags = graphene.List(TagArguments) 33 | addresses = graphene.List(AddressArguments) 34 | 35 | 36 | class CreateCompanyArguments(CompanyArguments): 37 | name = graphene.String(required=True) 38 | 39 | 40 | class UpdateCompanyArguments(CompanyArguments): 41 | uid = graphene.String(required=True) 42 | -------------------------------------------------------------------------------- /crm/apps/company/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/company/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/company/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import CompanyType 4 | 5 | from crm.graphql import BaseQuery 6 | 7 | 8 | 9 | class CompanyQuery(BaseQuery): 10 | companies = graphene.List(CompanyType) 11 | 12 | def resolve_comments(self, args, context, info): 13 | query = CompanyType.get_query(context) 14 | return query.all() 15 | -------------------------------------------------------------------------------- /crm/apps/company/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | 3 | from graphene_sqlalchemy import SQLAlchemyObjectType 4 | 5 | from crm.apps.company.models import CompanyTags 6 | 7 | 8 | class CompanyType(SQLAlchemyObjectType): 9 | 10 | class Meta: 11 | model = CompanyTags 12 | interfaces = (relay.Node,) 13 | name = model.__name__ 14 | -------------------------------------------------------------------------------- /crm/apps/company/models.py: -------------------------------------------------------------------------------- 1 | from crm.db import db, BaseModel, RootModel, ManyToManyBaseModel 2 | from crm.mailer import sendemail 3 | 4 | 5 | class CompanyTags(db.Model, ManyToManyBaseModel): 6 | __tablename__ = "companies_tags" 7 | 8 | tag_id = db.Column( 9 | db.String(5), 10 | db.ForeignKey('tags.id') 11 | ) 12 | 13 | company_id = db.Column( 14 | db.String(5), 15 | db.ForeignKey("companies.id") 16 | ) 17 | 18 | 19 | class Company(db.Model, BaseModel, RootModel): 20 | 21 | __tablename__ = "companies" 22 | 23 | name = db.Column( 24 | db.String(255), 25 | nullable=False, 26 | index=True 27 | ) 28 | 29 | # should be markdown. 30 | description = db.Column( 31 | db.Text(), 32 | default="", 33 | index=True 34 | ) 35 | 36 | vatnumber = db.Column( 37 | db.String(255), 38 | index=True 39 | ) 40 | 41 | website = db.Column( 42 | db.String(255), 43 | index=True 44 | ) 45 | 46 | emails = db.relationship( 47 | 'Email', 48 | backref='company', 49 | primaryjoin="Company.id==Email.company_id" 50 | ) 51 | 52 | telephones = db.relationship( 53 | 'Phone', 54 | backref='company', 55 | primaryjoin="Company.id==Phone.company_id" 56 | ) 57 | 58 | deals = db.relationship( 59 | "Deal", 60 | backref="company" 61 | ) 62 | 63 | messages = db.relationship( 64 | "Message", 65 | backref="company" 66 | ) 67 | 68 | tasks = db.relationship( 69 | "Task", 70 | backref="company" 71 | ) 72 | 73 | comments = db.relationship( 74 | "Comment", 75 | backref="company" 76 | ) 77 | 78 | contacts = db.relationship( 79 | "Contact", 80 | secondary="companies_contacts", 81 | backref='companies', 82 | ) 83 | 84 | owner_id = db.Column( 85 | db.String(5), 86 | db.ForeignKey('users.id') 87 | ) 88 | 89 | ownerbackup_id = db.Column( 90 | db.String(5), 91 | db.ForeignKey('users.id') 92 | ) 93 | 94 | links = db.relationship( 95 | "Link", 96 | backref="company" 97 | ) 98 | 99 | addresses = db.relationship( 100 | "Address", 101 | backref="company" 102 | ) 103 | 104 | @property 105 | def notification_emails(self): 106 | """ 107 | :return: list of all emails to send notifications to 108 | :rtype: list 109 | """ 110 | return [e.email for e in self.emails] 111 | 112 | def __str__(self): 113 | return self.name 114 | -------------------------------------------------------------------------------- /crm/apps/contact/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/contact/__init__.py -------------------------------------------------------------------------------- /crm/apps/contact/forms.py: -------------------------------------------------------------------------------- 1 | from wtforms_alchemy import ModelForm 2 | 3 | from .models import Contact 4 | 5 | 6 | class ContactForm(ModelForm): 7 | class Meta: 8 | model = Contact 9 | -------------------------------------------------------------------------------- /crm/apps/contact/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/contact/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/contact/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from graphene.types.inputobjecttype import InputObjectType 4 | from crm.apps.address.graphql.arguments import AddressArguments 5 | from crm.apps.comment.graphql.arguments import CommentArguments 6 | from crm.apps.contact.models import Gender 7 | from crm.apps.country.graphql.arguments import CountryArguments 8 | from crm.apps.link.graphql.arguments import LinkArguments 9 | from crm.apps.message.graphql.arguments import MessageArguments 10 | from crm.apps.passport.graphql.arguments import PassportArguments 11 | from crm.apps.task.graphql.arguments import TaskArguments 12 | from crm.graphql import BaseArgument 13 | 14 | 15 | class ContactSubgroupArguments(InputObjectType): 16 | groupname = graphene.String() 17 | 18 | 19 | class ContactArguments(InputObjectType, BaseArgument): 20 | uid = graphene.String() 21 | firstname = graphene.String() 22 | lastname = graphene.String() 23 | description = graphene.String() 24 | bio = graphene.String() 25 | belief_statement = graphene.String() 26 | gender = graphene.Enum.from_enum(Gender)() 27 | message_channels = graphene.String() 28 | owner = graphene.Argument('crm.apps.user.graphql.arguments.UserArguments') 29 | ownerbackup = graphene.Argument('crm.apps.user.graphql.arguments.UserArguments') 30 | parent = graphene.Argument('crm.apps.contact.graphql.arguments.ContactArguments') 31 | emails = graphene.String() 32 | telephones = graphene.String() 33 | tf_app = graphene.Boolean() 34 | tf_web = graphene.Boolean() 35 | referral_code = graphene.String() 36 | 37 | deals = graphene.List('crm.apps.deal.graphql.arguments.DealArguments') 38 | comments = graphene.List(CommentArguments) 39 | tasks = graphene.List(TaskArguments) 40 | messages = graphene.List(MessageArguments) 41 | links = graphene.List(LinkArguments) 42 | 43 | subgroups = graphene.List(ContactSubgroupArguments) 44 | addresses = graphene.List(AddressArguments) 45 | passports = graphene.List(PassportArguments) 46 | countries = graphene.List(CountryArguments) 47 | 48 | 49 | class CreateContactArguments(ContactArguments): 50 | firstname = graphene.String(required=True) 51 | lastname = graphene.String(required=True) 52 | 53 | 54 | class UpdateContactArguments(ContactArguments): 55 | uid = graphene.String(required=True) 56 | -------------------------------------------------------------------------------- /crm/apps/contact/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import relay 3 | 4 | from crm.apps.contact.graphql.arguments import ContactArguments 5 | from crm.apps.contact.graphql.types import ContactType 6 | from crm.graphql import BaseQuery, CRMConnectionField 7 | 8 | 9 | class ContactQuery(BaseQuery): 10 | """ 11 | we have 2 queries here contact and contacts 12 | """ 13 | 14 | # no need for resplve_contacts function here 15 | contacts = CRMConnectionField( 16 | ContactType, 17 | **ContactArguments.fields() 18 | 19 | ) 20 | # contact query to return one contact and takes (uid) argument 21 | # uid is the original object.id in db 22 | contact = graphene.Field(ContactType, uid=graphene.String()) 23 | 24 | def resolve_contact(self, context, uid): 25 | return ContactType.get_query(context).filter_by(id=uid).first() 26 | 27 | 28 | class Meta: 29 | interfaces = (relay.Node, ) -------------------------------------------------------------------------------- /crm/apps/contact/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | from graphene_sqlalchemy import SQLAlchemyObjectType 3 | 4 | from crm.apps.contact.models import Contact 5 | from crm.graphql import CrmType 6 | 7 | 8 | class ContactType(CrmType, SQLAlchemyObjectType): 9 | 10 | class Meta: 11 | model = Contact 12 | interfaces = (relay.Node,) 13 | name = model.__name__ 14 | 15 | # CrmType adds author_original, author_last objects rather than ids 16 | exclude_fields = ( 17 | 'author_original_id', 18 | 'author_last_id' 19 | ) 20 | -------------------------------------------------------------------------------- /crm/apps/country/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/country/__init__.py -------------------------------------------------------------------------------- /crm/apps/country/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/country/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/country/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import graphene 3 | from graphene.types.inputobjecttype import InputObjectType 4 | 5 | from crm.apps.country.countries import countries 6 | from crm.graphql import BaseArgument 7 | 8 | countries = enum.Enum('CountriesEnum', {v.replace(' ', '').replace('-', '').replace(',', '').replace("'", '').replace('.', '').replace('(', '').replace(')',''):k for k,v in countries.items()}) 9 | 10 | 11 | class CountryArguments(InputObjectType, BaseArgument): 12 | """ 13 | Country Arguments 14 | """ 15 | name = graphene.Enum.from_enum(countries)() 16 | 17 | # **************************************************** 18 | # NO NEED FOR CREATE/UPDATE COUNTRY * 19 | # ***************************************************** 20 | -------------------------------------------------------------------------------- /crm/apps/country/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/country/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/country/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import relay 3 | 4 | from crm.apps.country.graphql.arguments import CountryArguments 5 | from crm.apps.country.graphql.types import CountryType 6 | from crm.graphql import BaseQuery, CRMConnectionField 7 | 8 | 9 | class CountryQuery(BaseQuery): 10 | countries = CRMConnectionField( 11 | CountryType, 12 | **CountryArguments.fields() 13 | ) 14 | 15 | country = graphene.Field(CountryType, uid=graphene.String()) 16 | 17 | def resolve_country(self, context, uid): 18 | return CountryType.get_query(context).filter_by(id=uid).first() 19 | 20 | 21 | class Meta: 22 | interfaces = (relay.Node, ) -------------------------------------------------------------------------------- /crm/apps/country/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | from graphene_sqlalchemy import SQLAlchemyObjectType 3 | 4 | from crm.apps.country.models import Country 5 | from crm.graphql import CrmType 6 | 7 | 8 | class CountryType(CrmType, SQLAlchemyObjectType): 9 | 10 | class Meta: 11 | model = Country 12 | interfaces = (relay.Node,) 13 | name = model.__name__ 14 | # CrmType adds author_original, author_last objects rather than ids 15 | exclude_fields = ( 16 | 'author_original_id', 17 | 'author_last_id' 18 | ) 19 | -------------------------------------------------------------------------------- /crm/apps/country/models.py: -------------------------------------------------------------------------------- 1 | from crm.db import db, BaseModel 2 | 3 | from .countries import CountriesEnum 4 | 5 | 6 | class Country(db.Model, BaseModel): 7 | 8 | __tablename__ = "countries" 9 | 10 | name = db.Column( 11 | db.Enum(CountriesEnum), 12 | default=CountriesEnum.BE, 13 | unique=True, 14 | index=True 15 | ) 16 | 17 | contacts = db.relationship( 18 | "Contact", 19 | secondary="contacts_countries", 20 | backref="countries" 21 | ) 22 | 23 | addresses = db.relationship( 24 | "Address", 25 | backref="country" 26 | ) 27 | 28 | passports = db.relationship( 29 | "Passport", 30 | backref="country" 31 | ) 32 | 33 | def __str__(self): 34 | return self.name.value -------------------------------------------------------------------------------- /crm/apps/currency/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/currency/__init__.py -------------------------------------------------------------------------------- /crm/apps/currency/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/currency/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/currency/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | 5 | class CurrencyArguments(InputObjectType): 6 | """ 7 | currency Arguments 8 | """ 9 | name = graphene.String(required=True) 10 | value_usd = graphene.Float(required=True) 11 | -------------------------------------------------------------------------------- /crm/apps/currency/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/currency/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/currency/graphql/queries.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/currency/graphql/queries.py -------------------------------------------------------------------------------- /crm/apps/currency/graphql/types.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/currency/graphql/types.py -------------------------------------------------------------------------------- /crm/apps/currency/models.py: -------------------------------------------------------------------------------- 1 | from crm import db 2 | from crm.db import BaseModel, RootModel 3 | 4 | 5 | class Currency(db.Model, BaseModel, RootModel): 6 | 7 | __tablename__ = "currencies" 8 | 9 | name = db.Column( 10 | db.String(3), 11 | index=True, 12 | unique=True, 13 | nullable=False, 14 | ) 15 | 16 | value_usd = db.Column( 17 | db.Float(), 18 | default=1.0 19 | ) 20 | 21 | deals = db.relationship( 22 | "Deal", 23 | backref="currency" 24 | ) 25 | 26 | def __str__(self): 27 | return self.name 28 | -------------------------------------------------------------------------------- /crm/apps/deal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/deal/__init__.py -------------------------------------------------------------------------------- /crm/apps/deal/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/deal/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/deal/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | from crm.apps.address.graphql.arguments import AddressArguments 5 | from crm.apps.comment.graphql.arguments import CommentArguments 6 | from crm.apps.currency.graphql.arguments import CurrencyArguments 7 | from crm.apps.deal.models import DealType, DealState 8 | from crm.apps.link.graphql.arguments import LinkArguments 9 | from crm.apps.message.graphql.arguments import MessageArguments 10 | from crm.apps.task.graphql.arguments import TaskArguments 11 | from crm.graphql import BaseArgument 12 | 13 | DealType = graphene.Enum.from_enum(DealType) 14 | DealState = graphene.Enum.from_enum(DealState) 15 | 16 | 17 | class DealArguments(InputObjectType, BaseArgument): 18 | uid = graphene.String() 19 | name = graphene.String() 20 | description = graphene.String() 21 | value = graphene.String() 22 | currency = graphene.Argument(CurrencyArguments) 23 | deal_type = DealType() 24 | deal_state = DealState() 25 | closed_at = graphene.String() 26 | 27 | company_id = graphene.String() 28 | company = graphene.Argument('crm.apps.company.graphql.arguments.CompanyArguments') 29 | contact_id = graphene.String() 30 | contact = graphene.Argument('crm.apps.contact.graphql.arguments.ContactArguments') 31 | 32 | referrer1 = graphene.Argument('crm.apps.contact.graphql.arguments.ContactArguments') 33 | referrer2 = graphene.Argument('crm.apps.contact.graphql.arguments.ContactArguments') 34 | referrer1_id = graphene.String() 35 | referrer2_id = graphene.String() 36 | 37 | comments = graphene.List(CommentArguments) 38 | tasks = graphene.List(TaskArguments) 39 | messages = graphene.List(MessageArguments) 40 | links = graphene.List(LinkArguments) 41 | 42 | is_paid = graphene.Boolean() 43 | referral_code = graphene.String() 44 | shipping_addresses = graphene.List(AddressArguments) 45 | 46 | 47 | class CreateDealArguments(DealArguments): 48 | name = graphene.String(required=True) 49 | value = graphene.Float(required=True) 50 | currency = graphene.Argument(CurrencyArguments, required=True) 51 | deal_type = DealType(required=True) 52 | deal_state = DealState(required=True) 53 | 54 | 55 | class UpdateDealArguments(CreateDealArguments): 56 | uid = graphene.String(required=True) 57 | value = graphene.Float() -------------------------------------------------------------------------------- /crm/apps/deal/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import relay 3 | 4 | from crm.apps.deal.graphql.arguments import DealArguments 5 | from .types import DealType 6 | from crm.graphql import BaseQuery, CRMConnectionField 7 | 8 | 9 | 10 | class DealQuery(BaseQuery): 11 | """ 12 | we have 2 queries here Deal and Deals 13 | """ 14 | 15 | # no need for resplve_Deals function here 16 | deals = CRMConnectionField( 17 | DealType, 18 | DealArguments.fields() 19 | 20 | ) 21 | 22 | # Deal query to return one Deal and takes (uid) argument 23 | # uid is the original object.id in db 24 | deal = graphene.Field(DealType, uid=graphene.String()) 25 | 26 | def resolve_deal(self, context, uid): 27 | return DealType.get_query(context).filter_by(id=uid).first() 28 | 29 | class Meta: 30 | interfaces = (relay.Node,) 31 | -------------------------------------------------------------------------------- /crm/apps/deal/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | from graphene_sqlalchemy.types import SQLAlchemyObjectType 3 | 4 | from crm.apps.deal.models import Deal 5 | from crm.graphql import CrmType 6 | 7 | 8 | class DealType(CrmType, SQLAlchemyObjectType): 9 | class Meta: 10 | model = Deal 11 | interfaces = (relay.Node,) 12 | name = model.__name__ 13 | 14 | # CrmType adds author_original, author_last objects rather than ids 15 | exclude_fields = ( 16 | 'author_original_id', 17 | 'author_last_id' 18 | ) 19 | -------------------------------------------------------------------------------- /crm/apps/email/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/email/__init__.py -------------------------------------------------------------------------------- /crm/apps/email/models.py: -------------------------------------------------------------------------------- 1 | from crm import db, BaseModel 2 | 3 | 4 | class Email(db.Model, BaseModel): 5 | __tablename__ = "emails" 6 | 7 | email = db.Column( 8 | db.String(255), 9 | index=True, 10 | nullable=False, 11 | ) 12 | 13 | user_id = db.Column( 14 | db.String, 15 | db.ForeignKey("users.id") 16 | ) 17 | 18 | contact_id = db.Column( 19 | db.String, 20 | db.ForeignKey("contacts.id") 21 | ) 22 | 23 | organization_id = db.Column( 24 | db.String, 25 | db.ForeignKey("organizations.id") 26 | ) 27 | 28 | company_id = db.Column( 29 | db.String, 30 | db.ForeignKey("companies.id") 31 | ) 32 | 33 | def __str__(self): 34 | return self.email 35 | -------------------------------------------------------------------------------- /crm/apps/event/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/event/__init__.py -------------------------------------------------------------------------------- /crm/apps/event/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/event/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/event/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | from crm.apps.comment.graphql.arguments import CommentArguments 5 | from crm.apps.link.graphql.arguments import LinkArguments 6 | from crm.apps.message.graphql.arguments import MessageArguments 7 | from crm.apps.task.graphql.arguments import TaskArguments 8 | 9 | 10 | class EventArguments(InputObjectType): 11 | uid = graphene.String() 12 | title = graphene.String() 13 | description = graphene.String() 14 | contact_event_status = graphene.Float() 15 | event_datetime = graphene.String() 16 | 17 | comments = graphene.List(CommentArguments) 18 | tasks = graphene.List(TaskArguments) 19 | messages = graphene.List(MessageArguments) 20 | links = graphene.List(LinkArguments) 21 | contacts = graphene.List('crm.apps.contact.graphql.arguments.ContactArguments') 22 | 23 | 24 | class CreateEventArguments(EventArguments): 25 | title = graphene.String(required=True) 26 | 27 | 28 | class UpdateEventArguments(EventArguments): 29 | uid = graphene.String(required=False) -------------------------------------------------------------------------------- /crm/apps/event/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/event/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/event/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import EventType 4 | from crm.graphql import BaseQuery 5 | 6 | 7 | class EventQuery(BaseQuery): 8 | events = graphene.List(EventType) 9 | 10 | def resolve_events(self, args, context, info): 11 | query = EventType.get_query(context) 12 | return query.all() 13 | -------------------------------------------------------------------------------- /crm/apps/event/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | 3 | from graphene_sqlalchemy import SQLAlchemyObjectType 4 | 5 | from crm.apps.event.models import Event 6 | 7 | 8 | class EventType(SQLAlchemyObjectType): 9 | 10 | class Meta: 11 | model = Event 12 | interfaces = (relay.Node,) 13 | name = model.__name__ 14 | -------------------------------------------------------------------------------- /crm/apps/event/models.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from crm.db import db, BaseModel, ManyToManyBaseModel 3 | from datetime import datetime 4 | 5 | 6 | class ContactEventStatus(enum.Enum): 7 | INVITED = 'INVITED' 8 | WONTSHOW = 'WONTSHOW' 9 | ATTENDED = 'ATTENDED' 10 | DENIED = 'DENIED' 11 | COULDNTMAKEIT = 'COULDNTMAKEIT' 12 | 13 | ContactEventStatus.__str__ = lambda self: self.name 14 | 15 | 16 | class Event(db.Model, BaseModel): 17 | 18 | __tablename__ = "events" 19 | 20 | __mapper_args__ = {'polymorphic_identity': 'messages'} 21 | 22 | title = db.Column( 23 | db.String(255), 24 | nullable=False, 25 | index=True 26 | ) 27 | 28 | description = db.Column( 29 | db.Text(), 30 | default="", 31 | index=True 32 | ) 33 | contact_event_status = db.Column( 34 | db.Enum(ContactEventStatus), 35 | default=ContactEventStatus.INVITED, 36 | ) 37 | contacts = db.relationship( 38 | "Contact", 39 | secondary="contacts_events", 40 | backref="events" 41 | ) 42 | 43 | comments = db.relationship( 44 | "Comment", 45 | backref="event", 46 | ) 47 | 48 | messages = db.relationship( 49 | "Message", 50 | backref="event", 51 | ) 52 | 53 | links = db.relationship( 54 | "Link", 55 | backref="event", 56 | ) 57 | 58 | tasks = db.relationship( 59 | "Task", 60 | backref="event", 61 | ) 62 | event_datetime = db.Column( 63 | db.TIMESTAMP, 64 | default=datetime.utcnow, 65 | onupdate=datetime.utcnow, 66 | nullable=False, 67 | index=True 68 | ) 69 | 70 | @property 71 | def notification_emails(self): 72 | """ 73 | :return: list of all emails to send notifications to 74 | :rtype: list 75 | """ 76 | emails = [] 77 | if self.contacts: 78 | for contact in self.contacts: 79 | if contact.notification_emails: 80 | emails.extend(contact.notification_emails) 81 | if self.tasks: 82 | for task in self.tasks: 83 | if task.notification_emails: 84 | emails.extend(task.notification_emails) 85 | return list(set(emails)) 86 | 87 | def __str__(self): 88 | return self.title 89 | 90 | 91 | class ContactEvents(db.Model, ManyToManyBaseModel): 92 | 93 | __tablename__ = "contacts_events" 94 | 95 | event_id = db.Column( 96 | db.String(5), 97 | db.ForeignKey('events.id') 98 | ) 99 | 100 | contact_id = db.Column( 101 | db.String(5), 102 | db.ForeignKey("contacts.id") 103 | ) 104 | -------------------------------------------------------------------------------- /crm/apps/home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/home/__init__.py -------------------------------------------------------------------------------- /crm/apps/home/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/home/models.py -------------------------------------------------------------------------------- /crm/apps/home/templates/home/401.html: -------------------------------------------------------------------------------- 1 | 2 |

STOP :: You are not authorized

3 |

{{message|safe}}

4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /crm/apps/home/templates/home/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hey 4 | 5 | -------------------------------------------------------------------------------- /crm/apps/home/views.py: -------------------------------------------------------------------------------- 1 | from flask.templating import render_template 2 | 3 | from crm import app 4 | 5 | 6 | @app.route('/') 7 | def hello_world(): 8 | return render_template('home/index.html') 9 | -------------------------------------------------------------------------------- /crm/apps/image/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/image/__init__.py -------------------------------------------------------------------------------- /crm/apps/image/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/image/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/image/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | from graphene.types.inputobjecttype import InputObjectType 2 | 3 | 4 | class ImageArgument(InputObjectType): 5 | pass 6 | -------------------------------------------------------------------------------- /crm/apps/image/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/image/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/image/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import ImageType 4 | from crm.graphql import BaseQuery 5 | 6 | 7 | class ImageQuery(BaseQuery): 8 | images = graphene.List(ImageType) 9 | 10 | def resolve_images(self, args, context, info): 11 | query = ImageType.get_query(context) 12 | return query.all() 13 | -------------------------------------------------------------------------------- /crm/apps/image/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | 3 | from graphene_sqlalchemy import SQLAlchemyObjectType 4 | 5 | from crm.apps.image.models import Image 6 | 7 | 8 | class ImageType(SQLAlchemyObjectType): 9 | 10 | class Meta: 11 | model = Image 12 | interfaces = (relay.Node,) 13 | name = model.__name__ 14 | -------------------------------------------------------------------------------- /crm/apps/image/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | from crm.db import db, BaseModel 3 | from crm.settings import IMAGES_DIR, STATIC_URL_PATH 4 | 5 | 6 | class Image(db.Model, BaseModel): 7 | 8 | __tablename__ = "images" 9 | 10 | name = db.Column( 11 | db.String(255) 12 | ) 13 | 14 | path = db.Column( 15 | db.String(255) 16 | ) 17 | 18 | contact_id = db.Column( 19 | db.String(5), 20 | db.ForeignKey('contacts.id') 21 | ) 22 | 23 | @property 24 | def imgurl(self): 25 | return os.path.join(STATIC_URL_PATH, "uploads", "images", self.path) 26 | 27 | @property 28 | def fullpath(self): 29 | return os.path.join(IMAGES_DIR, self.path) 30 | 31 | @property 32 | def as_image(self): 33 | return ''.format(imgurl=self.imgurl) 34 | 35 | def __str__(self): 36 | return self.path 37 | -------------------------------------------------------------------------------- /crm/apps/knowledge/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/knowledge/__init__.py -------------------------------------------------------------------------------- /crm/apps/knowledge/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/knowledge/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/knowledge/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | from graphene.types.inputobjecttype import InputObjectType 2 | 3 | 4 | class KnowledgebaseArgument(InputObjectType): 5 | pass 6 | -------------------------------------------------------------------------------- /crm/apps/knowledge/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/knowledge/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/knowledge/graphql/queries.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/knowledge/graphql/queries.py -------------------------------------------------------------------------------- /crm/apps/knowledge/graphql/types.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/knowledge/graphql/types.py -------------------------------------------------------------------------------- /crm/apps/knowledge/models.py: -------------------------------------------------------------------------------- 1 | from crm.db import db, BaseModel 2 | 3 | 4 | class KnowledgeBase(db.Model, BaseModel): 5 | 6 | __tablename__ = "knowledgebases" 7 | 8 | title = db.Column( 9 | db.String(255), 10 | nullable=False, 11 | index=True 12 | ) 13 | 14 | category_id = db.Column( 15 | db.String(5), 16 | db.ForeignKey("knowledgebase_categories.id") 17 | ) 18 | 19 | author_id = db.Column( 20 | db.String(5), 21 | db.ForeignKey("users.id") 22 | ) 23 | 24 | content = db.Column( 25 | db.Text(), 26 | index=True 27 | ) 28 | 29 | tasks = db.relationship( 30 | "Task", 31 | backref="knowledge_base" 32 | ) 33 | 34 | comments = db.relationship( 35 | "Comment", 36 | backref="knowledge_base" 37 | ) 38 | 39 | def __str__(self): 40 | return self.title 41 | 42 | 43 | class KnowledgeBaseCategory(db.Model, BaseModel): 44 | 45 | __tablename__ = "knowledgebase_categories" 46 | 47 | name = db.Column( 48 | db.String(255), 49 | nullable=False, 50 | index=True 51 | ) 52 | 53 | description = db.Column( 54 | db.Text() 55 | ) 56 | 57 | knowledge_bases = db.relationship( 58 | "KnowledgeBase", 59 | backref="category" 60 | ) 61 | 62 | def __str__(self): 63 | return self.name 64 | -------------------------------------------------------------------------------- /crm/apps/link/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/link/__init__.py -------------------------------------------------------------------------------- /crm/apps/link/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/link/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/link/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | 5 | class LinkArguments(InputObjectType): 6 | uid = graphene.String() 7 | url = graphene.String() 8 | labels = graphene.String() 9 | contact_id = graphene.String() 10 | user_id = graphene.String() 11 | deal_id = graphene.String() 12 | task_id = graphene.String() 13 | organization_id = graphene.String() 14 | project_id = graphene.String() 15 | sprint_id = graphene.String() 16 | event_id = graphene.String() 17 | comments = graphene.List('crm.apps.comment.graphql.arguments.CommentArguments') 18 | 19 | 20 | class CreateLinkArguments(LinkArguments): 21 | url = graphene.String(required=True) 22 | 23 | 24 | class UpdateLinkArguments(LinkArguments): 25 | uid = graphene.String(requied=True) -------------------------------------------------------------------------------- /crm/apps/link/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/link/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/link/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import LinkType 4 | from crm.graphql import BaseQuery 5 | 6 | 7 | class LinkQuery(BaseQuery): 8 | links = graphene.List(LinkType) 9 | 10 | def resolve_links(self, args, context, info): 11 | query = LinkType.get_query(context) 12 | return query.all() 13 | -------------------------------------------------------------------------------- /crm/apps/link/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene_sqlalchemy import SQLAlchemyObjectType 2 | 3 | from crm.apps.link.models import Link 4 | 5 | 6 | class LinkType(SQLAlchemyObjectType): 7 | 8 | class Meta: 9 | model = Link 10 | 11 | -------------------------------------------------------------------------------- /crm/apps/link/models.py: -------------------------------------------------------------------------------- 1 | from crm.db import db, BaseModel 2 | 3 | 4 | class Link(db.Model, BaseModel): 5 | 6 | __tablename__ = "links" 7 | 8 | url = db.Column( 9 | db.String(255), 10 | nullable=False, 11 | index=True 12 | ) 13 | 14 | filename =db.Column( 15 | db.String() 16 | ) 17 | 18 | labels = db.Column( 19 | db.Text(), 20 | index=True 21 | ) 22 | 23 | contact_id = db.Column( 24 | db.String, 25 | db.ForeignKey("contacts.id") 26 | ) 27 | 28 | user_id = db.Column( 29 | db.String, 30 | db.ForeignKey("users.id") 31 | ) 32 | 33 | deal_id = db.Column( 34 | db.String, 35 | db.ForeignKey("deals.id") 36 | ) 37 | 38 | task_id = db.Column( 39 | db.String, 40 | db.ForeignKey("tasks.id") 41 | ) 42 | 43 | organization_id = db.Column( 44 | db.String, 45 | db.ForeignKey("organizations.id") 46 | ) 47 | 48 | project_id = db.Column( 49 | db.String, 50 | db.ForeignKey("projects.id") 51 | ) 52 | 53 | sprint_id = db.Column( 54 | db.String, 55 | db.ForeignKey("sprints.id") 56 | ) 57 | 58 | company_id = db.Column( 59 | db.String, 60 | db.ForeignKey("companies.id") 61 | ) 62 | event_id = db.Column( 63 | db.String, 64 | db.ForeignKey("events.id") 65 | ) 66 | message_id = db.Column( 67 | db.String(), 68 | db.ForeignKey("messages.id") 69 | ) 70 | 71 | # alert_id = db.Column( 72 | # db.String, 73 | # db.ForeignKey("alerts.id") 74 | # ) 75 | # 76 | # alert_source_id = db.Column( 77 | # db.String, 78 | # db.ForeignKey("alertsources.id") 79 | # ) 80 | 81 | comments = db.relationship( 82 | "Comment", 83 | backref="link" 84 | ) 85 | 86 | def __str__(self): 87 | return self.filename or self.url 88 | -------------------------------------------------------------------------------- /crm/apps/message/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/message/__init__.py -------------------------------------------------------------------------------- /crm/apps/message/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/message/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/message/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | 5 | class MessageArguments(InputObjectType): 6 | uid = graphene.String() 7 | title = graphene.String() 8 | content = graphene.String() 9 | channel = graphene.String() 10 | time_tosend = graphene.String() 11 | time_sent = graphene.String() 12 | author = graphene.Field('crm.apps.contact.graphql.arguments.ContactArguments') 13 | contact = graphene.Field('crm.apps.contact.graphql.arguments.ContactArguments') 14 | user = graphene.Field('crm.apps.user.graphql.arguments.UserArguments') 15 | deal = graphene.Field('crm.apps.deal.graphql.arguments.DealArguments') 16 | task = graphene.Field('crm.apps.task.graphql.arguments.TaskArguments') 17 | organization = graphene.Field('crm.apps.organization.graphql.arguments.OrganizationArguments') 18 | project = graphene.Field('crm.apps.project.graphql.arguments.ProjectArguments') 19 | sprint = graphene.Field('crm.apps.sprint.graphql.arguments.SprintArguments') 20 | event_id = graphene.Field('crm.apps.event.graphql.arguments.EventArguments') 21 | comments = graphene.List('crm.apps.comment.graphql.arguments.CommentArguments') 22 | 23 | 24 | class CreateMessageArguments(MessageArguments): 25 | title = graphene.String(required=True) 26 | 27 | 28 | class UpdateMessageArguments(MessageArguments): 29 | uid = graphene.String(requied=True) -------------------------------------------------------------------------------- /crm/apps/message/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/message/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/message/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import MessageType 4 | 5 | from crm.graphql import BaseQuery 6 | 7 | 8 | class MessageQuery(BaseQuery): 9 | messages = graphene.List(MessageType) 10 | 11 | def resolve_messages(self, args, context, info): 12 | query = MessageType.get_query(context) 13 | return query.all() 14 | -------------------------------------------------------------------------------- /crm/apps/message/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | 3 | from graphene_sqlalchemy import SQLAlchemyObjectType 4 | 5 | from crm.apps.message.models import Message 6 | 7 | 8 | class MessageType(SQLAlchemyObjectType): 9 | 10 | class Meta: 11 | model = Message 12 | interfaces = (relay.Node,) 13 | name = model.__name__ 14 | -------------------------------------------------------------------------------- /crm/apps/organization/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/organization/__init__.py -------------------------------------------------------------------------------- /crm/apps/organization/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/organization/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/organization/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | 5 | class OrganizationArguments(InputObjectType): 6 | uid = graphene.String() 7 | name = graphene.String() 8 | description = graphene.String() 9 | emails = graphene.String() 10 | telephones = graphene.String() 11 | tasks = graphene.List('crm.apps.task.graphql.arguments.TaskArguments') 12 | comments = graphene.List('crm.apps.comment.graphql.arguments.CommentArguments') 13 | messages = graphene.List('crm.apps.message.graphql.arguments.MessageArguments') 14 | users = graphene.List('crm.apps.user.graphql.arguments.UserArguments') 15 | links = graphene.List('crm.apps.link.graphql.arguments.LinkArguments') 16 | owner_id = graphene.String() 17 | parent_id = graphene.String() 18 | 19 | 20 | class CreateOrganizationArguments(OrganizationArguments): 21 | name = graphene.String(required=True) 22 | 23 | 24 | class UpdateOrganizationArguments(OrganizationArguments): 25 | uid = graphene.String(required=True) 26 | -------------------------------------------------------------------------------- /crm/apps/organization/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/organization/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/organization/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import OrganizationType 4 | from crm.graphql import BaseQuery 5 | 6 | 7 | class OrganizationQuery(BaseQuery): 8 | organizations = graphene.List(OrganizationType) 9 | 10 | def resolve_organizations(self, args, context, info): 11 | query = OrganizationType.get_query(context) 12 | return query.all() 13 | -------------------------------------------------------------------------------- /crm/apps/organization/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | from graphene_sqlalchemy import SQLAlchemyObjectType 3 | 4 | from crm.apps.organization.models import Organization 5 | 6 | 7 | class OrganizationType(SQLAlchemyObjectType): 8 | 9 | class Meta: 10 | model = Organization 11 | interfaces = (relay.Node,) 12 | name = model.__name__ 13 | -------------------------------------------------------------------------------- /crm/apps/organization/models.py: -------------------------------------------------------------------------------- 1 | from crm.db import db, BaseModel, RootModel 2 | from crm.mailer import sendemail 3 | 4 | 5 | class Organization(db.Model, BaseModel, RootModel): 6 | 7 | __tablename__ = "organizations" 8 | 9 | name = db.Column( 10 | db.String(255), 11 | nullable=False, 12 | index=True 13 | ) 14 | 15 | # should be markdown 16 | description = db.Column( 17 | db.Text(), 18 | default="", 19 | index=True 20 | ) 21 | 22 | emails = db.relationship( 23 | 'Email', 24 | backref='organization', 25 | primaryjoin="Organization.id==Email.organization_id" 26 | ) 27 | 28 | tasks = db.relationship( 29 | "Task", 30 | backref="organization" 31 | ) 32 | 33 | comments = db.relationship( 34 | "Comment", 35 | backref="organization" 36 | ) 37 | 38 | users = db.relationship( 39 | "User", 40 | secondary="users_organizations", 41 | secondaryjoin="User.id==UsersOrganizations.user_id", 42 | backref="organizations" 43 | ) 44 | 45 | links = db.relationship( 46 | "Link", 47 | backref="organization" 48 | ) 49 | 50 | messages = db.relationship( 51 | "Message", 52 | backref="organization" 53 | ) 54 | 55 | owner_id = db.Column( 56 | db.String(5), 57 | db.ForeignKey('users.id') 58 | ) 59 | 60 | parent_id = db.Column( 61 | db.String(5), 62 | db.ForeignKey("organizations.id") 63 | ) 64 | 65 | @property 66 | def notification_emails(self): 67 | """ 68 | :return: list of all emails to send notifications to 69 | :rtype: list 70 | """ 71 | return [e.email for e in self.emails] 72 | 73 | def __str__(self): 74 | return self.name 75 | -------------------------------------------------------------------------------- /crm/apps/passport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/passport/__init__.py -------------------------------------------------------------------------------- /crm/apps/passport/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/passport/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/passport/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | from crm.apps.country.graphql.arguments import CountryArguments 5 | 6 | 7 | class PassportArguments(InputObjectType): 8 | passport_fullname = graphene.String() 9 | passport_number = graphene.String() 10 | issuance_date = graphene.String() 11 | expiration_date = graphene.String() 12 | country = graphene.Argument(CountryArguments) 13 | contact = graphene.Argument('crm.apps.contact.graphql.arguments.ContactArguments') 14 | contact_id = graphene.String() 15 | 16 | 17 | class CreateContactArguments(PassportArguments): 18 | passport_fullname = graphene.String(required=True) 19 | passport_number = graphene.String(required=True) 20 | issuance_date = graphene.String(required=True) 21 | expiration_date = graphene.String(required=True) 22 | 23 | 24 | class UpdateContactArguments(PassportArguments): 25 | uid = graphene.String(required=True) 26 | -------------------------------------------------------------------------------- /crm/apps/passport/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/passport/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/passport/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import PassportType 4 | from crm.graphql import BaseQuery 5 | 6 | 7 | class PassportQuery(BaseQuery): 8 | passports = graphene.List(PassportType) 9 | 10 | def resolve_passports(self, args, context, info): 11 | query = PassportType.get_query(context) 12 | return query.all() 13 | -------------------------------------------------------------------------------- /crm/apps/passport/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | 3 | from graphene_sqlalchemy import SQLAlchemyObjectType 4 | 5 | from crm.apps.passport.models import Passport 6 | from crm.graphql import CrmType 7 | 8 | 9 | class PassportType(CrmType, SQLAlchemyObjectType): 10 | 11 | class Meta: 12 | model = Passport 13 | interfaces = (relay.Node,) 14 | name = model.__name__ 15 | 16 | # CrmType adds author_original, author_last objects rather than ids 17 | exclude_fields = ( 18 | 'author_original_id', 19 | 'author_last_id' 20 | ) 21 | 22 | -------------------------------------------------------------------------------- /crm/apps/passport/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from crm.db import db, BaseModel 3 | 4 | 5 | class Passport(db.Model, BaseModel): 6 | 7 | __tablename__ = "passports" 8 | 9 | passport_fullname = db.Column( 10 | db.String(255), 11 | nullable=False, 12 | index=True 13 | ) 14 | 15 | passport_number = db.Column( 16 | db.Text(), 17 | index=True, 18 | nullable=False 19 | ) 20 | 21 | issuance_date = db.Column( 22 | db.Date(), 23 | default=datetime.date(1990, 1, 1), 24 | nullable=False 25 | ) 26 | expiration_date = db.Column( 27 | db.Date(), 28 | default=datetime.date(2020, 1, 1), 29 | nullable=False 30 | ) 31 | country_id = db.Column( 32 | db.String(5), 33 | db.ForeignKey('countries.id') 34 | ) 35 | 36 | contact_id = db.Column( 37 | db.String(5), 38 | db.ForeignKey('contacts.id') 39 | ) 40 | 41 | def __str__(self): 42 | return "Passport {}".format(self.passport_fullname) 43 | -------------------------------------------------------------------------------- /crm/apps/phone/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/phone/__init__.py -------------------------------------------------------------------------------- /crm/apps/phone/models.py: -------------------------------------------------------------------------------- 1 | from crm import db, BaseModel 2 | 3 | 4 | class Phone(db.Model, BaseModel): 5 | __tablename__ = "phones" 6 | 7 | telephone = db.Column( 8 | db.String(255), 9 | index=True, 10 | nullable=False, 11 | ) 12 | 13 | user_id = db.Column( 14 | db.String, 15 | db.ForeignKey("users.id") 16 | ) 17 | 18 | contact_id = db.Column( 19 | db.String, 20 | db.ForeignKey("contacts.id") 21 | ) 22 | 23 | company_id = db.Column( 24 | db.String, 25 | db.ForeignKey("companies.id") 26 | ) 27 | 28 | def __str__(self): 29 | return self.telephone 30 | 31 | -------------------------------------------------------------------------------- /crm/apps/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/project/__init__.py -------------------------------------------------------------------------------- /crm/apps/project/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/project/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/project/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | 5 | class ProjectArguments(InputObjectType): 6 | uid = graphene.String() 7 | name = graphene.String() 8 | description = graphene.String() 9 | start_date = graphene.String() 10 | deadline = graphene.String() 11 | 12 | comments = graphene.List('crm.apps.comment.graphql.arguments.CommentArguments') 13 | messages = graphene.List('crm.apps.message.graphql.arguments.MessageArguments') 14 | links = graphene.List('crm.apps.link.graphql.arguments.LinkArguments') 15 | tasks = graphene.List('crm.apps.task.graphql.arguments.TaskArguments') 16 | sprints = graphene.List('crm.apps.sprint.graphql.arguments.SprintArguments') 17 | contacts = graphene.List('crm.apps.contact.graphql.arguments.ContactArguments') 18 | promoter_id = graphene.String() 19 | guardian_id = graphene.String() 20 | 21 | 22 | class CreateProjectArguments(ProjectArguments): 23 | name = graphene.String(required=True) 24 | 25 | 26 | class UpdateProjectArguments(ProjectArguments): 27 | uid = graphene.String(requied=True) 28 | -------------------------------------------------------------------------------- /crm/apps/project/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/project/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/project/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import ProjectType 4 | 5 | from crm.graphql import BaseQuery 6 | 7 | 8 | class ProjectQuery(BaseQuery): 9 | projects = graphene.List(ProjectType) 10 | 11 | def resolve_projects(self, args, context, info): 12 | query = ProjectType.get_query(context) 13 | return query.all() 14 | -------------------------------------------------------------------------------- /crm/apps/project/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | 3 | from graphene_sqlalchemy import SQLAlchemyObjectType 4 | 5 | from crm.apps.project.models import Project 6 | 7 | 8 | class ProjectType(SQLAlchemyObjectType): 9 | 10 | class Meta: 11 | model = Project 12 | interfaces = (relay.Node,) 13 | name = model.__name__ 14 | -------------------------------------------------------------------------------- /crm/apps/reports/views.py: -------------------------------------------------------------------------------- 1 | from flask_admin import BaseView, Admin 2 | 3 | 4 | from crm import app 5 | 6 | 7 | class ReportsAdminView(BaseView): 8 | def __init__(self, *args, **kwargs): 9 | self._default_view = True 10 | super(ReportsAdminView, self).__init__(*args, **kwargs) 11 | self.admin = app.admin 12 | 13 | 14 | @app.route('/reports', methods=["GET"]) 15 | def reports(): 16 | return ReportsAdminView().render('reports/reports.html'), 200 17 | -------------------------------------------------------------------------------- /crm/apps/sprint/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/sprint/__init__.py -------------------------------------------------------------------------------- /crm/apps/sprint/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/sprint/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/sprint/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | 5 | class SprintArguments(InputObjectType): 6 | uid = graphene.String() 7 | name = graphene.String() 8 | description = graphene.String() 9 | start_date = graphene.String() 10 | deadline = graphene.String() 11 | contacts = graphene.List('crm.apps.contact.graphql.arguments.ContactArguments') 12 | tasks = graphene.List('crm.apps.task.graphql.arguments.TaskArguments') 13 | comments = graphene.List('crm.apps.comment.graphql.arguments.CommentArguments') 14 | messages = graphene.List('crm.apps.message.graphql.arguments.MessageArguments') 15 | links = graphene.List('crm.apps.link.graphql.arguments.LinkArguments') 16 | owner_id = graphene.String() 17 | project_id = graphene.String() 18 | 19 | 20 | class CreateSprintArguments(SprintArguments): 21 | name = graphene.String(required=True) 22 | 23 | 24 | class UpdateSprintArguments(SprintArguments): 25 | uid = graphene.String(required=True) 26 | -------------------------------------------------------------------------------- /crm/apps/sprint/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/sprint/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/sprint/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import SprintType 4 | 5 | from crm.graphql import BaseQuery 6 | 7 | 8 | class SprintQuery(BaseQuery): 9 | sprints = graphene.List(SprintType) 10 | 11 | def resolve_sprints(self, args, context, info): 12 | query = SprintType.get_query(context) 13 | return query.all() 14 | -------------------------------------------------------------------------------- /crm/apps/sprint/graphql/types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import relay 3 | 4 | from graphene_sqlalchemy import SQLAlchemyObjectType 5 | 6 | from crm.apps.sprint.models import Sprint 7 | 8 | 9 | class SprintType(SQLAlchemyObjectType): 10 | 11 | class Meta: 12 | model = Sprint 13 | interfaces = (relay.Node,) 14 | name = model.__name__ 15 | 16 | -------------------------------------------------------------------------------- /crm/apps/sprint/models.py: -------------------------------------------------------------------------------- 1 | from crm.db import db, BaseModel, RootModel 2 | from crm.mailer import sendemail 3 | 4 | 5 | class Sprint(db.Model, BaseModel, RootModel): 6 | 7 | __tablename__ = "sprints" 8 | 9 | name = db.Column( 10 | db.String(255), 11 | nullable=False, 12 | index=True 13 | ) 14 | 15 | # should be markdown. 16 | description = db.Column( 17 | db.Text(), 18 | default="", 19 | index=True 20 | ) 21 | 22 | start_date = db.Column( 23 | db.TIMESTAMP, 24 | index=True 25 | ) 26 | 27 | deadline = db.Column( 28 | db.TIMESTAMP, 29 | index=True 30 | ) 31 | 32 | contacts = db.relationship( 33 | "Contact", 34 | secondary="contacts_sprints", 35 | backref="sprints" 36 | ) 37 | 38 | tasks = db.relationship( 39 | "Task", 40 | backref="sprint" 41 | ) 42 | 43 | comments = db.relationship( 44 | "Comment", 45 | backref="sprint" 46 | ) 47 | 48 | links = db.relationship( 49 | "Link", 50 | backref="sprint" 51 | ) 52 | 53 | messages = db.relationship( 54 | "Message", 55 | backref="sprint" 56 | ) 57 | 58 | owner_id = db.Column( 59 | db.String(5), 60 | db.ForeignKey('users.id') 61 | ) 62 | 63 | project_id = db.Column( 64 | db.String(5), 65 | db.ForeignKey('projects.id') 66 | ) 67 | 68 | @property 69 | def notification_emails(self): 70 | """ 71 | :return: list of all emails to send notifications to 72 | :rtype: list 73 | """ 74 | emails = [] 75 | 76 | if self.contacts: 77 | for contact in self.contacts: 78 | if contact.notification_emails: 79 | emails.extend(contact.notification_emails) 80 | 81 | if self.owner: 82 | if self.owner.notification_emails: 83 | emails.extend(self.owner.notification_emails) 84 | 85 | if self.tasks: 86 | for task in self.tasks: 87 | if task.notification_emails: 88 | emails.extend(task.notification_emails) 89 | 90 | return list(set(emails)) 91 | 92 | @property 93 | def percentage_done(self): 94 | pass 95 | 96 | @property 97 | def hours_open(self): 98 | pass 99 | 100 | @property 101 | def hours_open_person_avg(self): 102 | pass 103 | 104 | @property 105 | def hours_open_person_max(self): 106 | pass 107 | 108 | def __str__(self): 109 | return self.name 110 | -------------------------------------------------------------------------------- /crm/apps/tag/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/tag/__init__.py -------------------------------------------------------------------------------- /crm/apps/tag/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/tag/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/tag/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | 5 | class TagArguments(InputObjectType): 6 | tag = graphene.String() 7 | 8 | -------------------------------------------------------------------------------- /crm/apps/tag/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/tag/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/tag/graphql/queries.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/tag/graphql/queries.py -------------------------------------------------------------------------------- /crm/apps/tag/graphql/types.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/tag/graphql/types.py -------------------------------------------------------------------------------- /crm/apps/tag/models.py: -------------------------------------------------------------------------------- 1 | from crm.db import db, BaseModel 2 | 3 | 4 | class Tag(db.Model, BaseModel): 5 | """ 6 | Model for any kind of tags 7 | """ 8 | 9 | __tablename__ = "tags" 10 | 11 | tag = db.Column( 12 | db.String(), 13 | nullable=False, 14 | unique=True, 15 | index=True 16 | ) 17 | 18 | companies = db.relationship( 19 | "Company", 20 | secondary="companies_tags", 21 | backref="tags" 22 | ) 23 | 24 | def __str__(self): 25 | return self.tag -------------------------------------------------------------------------------- /crm/apps/task/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/task/__init__.py -------------------------------------------------------------------------------- /crm/apps/task/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/task/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/task/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | from crm.apps.task.models import TaskType, TaskPriority, TaskState 5 | 6 | 7 | class TaskArguments(InputObjectType): 8 | uid = graphene.String() 9 | title = graphene.String() 10 | description = graphene.String() 11 | type = graphene.Enum.from_enum(TaskType) 12 | priority = graphene.Enum.from_enum(TaskPriority) 13 | state = graphene.Enum.from_enum(TaskState) 14 | 15 | assignee = graphene.Field('crm.apps.user.graphql.arguments.UserArguments') 16 | deadline = graphene.String() 17 | eta = graphene.String() 18 | time_estimate = graphene.Int() 19 | time_done = graphene.Int() 20 | company = graphene.Field('crm.apps.company.graphql.arguments.CompanyArguments') 21 | contact = graphene.Field('crm.apps.contact.graphql.arguments.ContactArguments') 22 | user = graphene.Field('crm.apps.user.graphql.arguments.UserArguments') 23 | deal = graphene.Field('crm.apps.deal.graphql.arguments.DealArguments') 24 | organization = graphene.Field('crm.apps.organization.graphql.arguments.OrganizationArguments') 25 | project = graphene.Field('crm.apps.project.graphql.arguments.ProjectArguments') 26 | sprint = graphene.Field('crm.apps.sprint.graphql.arguments.SprintArguments') 27 | event = graphene.Field('crm.apps.event.graphql.arguments.EventArguments') 28 | comments = graphene.List('crm.apps.comment.graphql.arguments.CommentArguments') 29 | messages = graphene.List('crm.apps.message.graphql.arguments.MessageArguments') 30 | links = graphene.List('crm.apps.link.graphql.arguments.LinkArguments') 31 | 32 | 33 | class CreateTaskArguments(TaskArguments): 34 | title = graphene.String(required=True) 35 | 36 | 37 | class UpdateTaskArguments(TaskArguments): 38 | uid = graphene.String(requied=True) -------------------------------------------------------------------------------- /crm/apps/task/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/task/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/task/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .types import TaskType 4 | 5 | from crm.graphql import BaseQuery 6 | 7 | 8 | class TaskQuery(BaseQuery): 9 | tasks = graphene.List(TaskType) 10 | 11 | def resolve_tasks(self, args, context, info): 12 | query = TaskType.get_query(context) 13 | return query.all() 14 | -------------------------------------------------------------------------------- /crm/apps/task/graphql/types.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import relay 3 | 4 | from graphene_sqlalchemy import SQLAlchemyObjectType 5 | 6 | from crm.apps.task.models import Task 7 | 8 | 9 | class TaskType(SQLAlchemyObjectType): 10 | 11 | uid = graphene.String() 12 | 13 | class Meta: 14 | model = Task 15 | interfaces = (relay.Node,) 16 | name = model.__name__ -------------------------------------------------------------------------------- /crm/apps/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/user/__init__.py -------------------------------------------------------------------------------- /crm/apps/user/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/user/graphql/__init__.py -------------------------------------------------------------------------------- /crm/apps/user/graphql/arguments.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene.types.inputobjecttype import InputObjectType 3 | 4 | 5 | class UserArguments(InputObjectType): 6 | uid = graphene.String() 7 | username = graphene.String() 8 | firstname = graphene.String() 9 | lastname = graphene.String() 10 | description = graphene.String() 11 | message_channels = graphene.String() 12 | emails = graphene.String() 13 | telephones = graphene.String() 14 | tasks = graphene.List('crm.apps.task.graphql.arguments.TaskArguments') 15 | owns_tasks = graphene.List('crm.apps.task.graphql.arguments.TaskArguments') 16 | comments = graphene.List('crm.apps.comment.graphql.arguments.CommentArguments') 17 | messages = graphene.List('crm.apps.message.graphql.arguments.MessageArguments') 18 | links = graphene.List('crm.apps.link.graphql.arguments.LinkArguments') 19 | owns_contacts = graphene.List('crm.apps.contact.graphql.arguments.ContactArguments') 20 | owns_backup_contacts = graphene.List('crm.apps.contact.graphql.arguments.ContactArguments') 21 | owns_companies = graphene.List('crm.apps.company.graphql.arguments.CompanyArguments') 22 | owns_backup_companies = graphene.List('crm.apps.company.graphql.arguments.CompanyArguments') 23 | owns_organizations = graphene.List('crm.apps.organization.graphql.arguments.OrganizationArguments') 24 | owns_sprints = graphene.List('crm.apps.sprint.graphql.arguments.SprintArguments') 25 | promoter_projects = graphene.List('crm.apps.project.graphql.arguments.ProjectArguments') 26 | guardian_projects = graphene.List('crm.apps.project.graphql.arguments.ProjectArguments') 27 | 28 | 29 | class CreateUserArguments(UserArguments): 30 | username = graphene.String(required=True) 31 | 32 | 33 | class UpdateUserArguments(UserArguments): 34 | uid = graphene.String(requied=True) -------------------------------------------------------------------------------- /crm/apps/user/graphql/mutations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/apps/user/graphql/mutations.py -------------------------------------------------------------------------------- /crm/apps/user/graphql/queries.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from .types import UserType 3 | 4 | from crm.graphql import BaseQuery 5 | 6 | 7 | class UserQuery(BaseQuery): 8 | users = graphene.List(UserType) 9 | 10 | def resolve_users(self, args, context, info): 11 | query = UserType.get_query(context) 12 | return query.all() 13 | -------------------------------------------------------------------------------- /crm/apps/user/graphql/types.py: -------------------------------------------------------------------------------- 1 | from graphene import relay 2 | 3 | from graphene_sqlalchemy import SQLAlchemyObjectType 4 | 5 | from crm.apps.user.models import User 6 | from crm.graphql import CrmType 7 | 8 | 9 | class UserType(CrmType, SQLAlchemyObjectType): 10 | 11 | class Meta: 12 | model = User 13 | interfaces = (relay.Node,) 14 | name = model.__name__ 15 | exclude_fields = ( 16 | 'author_original_id', 17 | 'author_last_id' 18 | ) 19 | -------------------------------------------------------------------------------- /crm/apps/user/tasks.py: -------------------------------------------------------------------------------- 1 | # RQ tasks 2 | 3 | 4 | def update_last_login_time(user_id, time): 5 | """ 6 | Update last login time. 7 | Used in iyo middleware 8 | :param user_id: user ID 9 | :param time: last login time 10 | """ 11 | from crm.db import db 12 | from .models import User 13 | 14 | u = User.query.filter_by(id=user_id).first() 15 | u.last_login = time 16 | db.session.add(u) 17 | db.session.commit() 18 | 19 | -------------------------------------------------------------------------------- /crm/cli/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Import and expose all modules in this package. 3 | Now any added module/command will be exposed automatically 4 | """ 5 | 6 | import pkgutil 7 | import inspect 8 | 9 | __all__ = [] 10 | 11 | for loader, name, is_pkg in pkgutil.walk_packages(__path__): 12 | module = loader.find_module(name).load_module(name) 13 | 14 | for name, value in inspect.getmembers(module): 15 | if name.startswith('__'): 16 | continue 17 | 18 | globals()[name] = value 19 | __all__.append(name) 20 | -------------------------------------------------------------------------------- /crm/cli/createdb.py: -------------------------------------------------------------------------------- 1 | from crm import app 2 | 3 | from sqlalchemy_utils import create_database, database_exists 4 | 5 | 6 | @app.cli.command() 7 | def createdb(): 8 | """ 9 | Create DB 10 | """ 11 | if not database_exists(app.config['SQLALCHEMY_DATABASE_URI']): 12 | create_database(app.config['SQLALCHEMY_DATABASE_URI']) 13 | print("DB created.") 14 | -------------------------------------------------------------------------------- /crm/cli/dumpdata.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import ujson as json 4 | 5 | from crm import app 6 | from crm.db import RootModel 7 | 8 | 9 | @app.cli.command() 10 | def dumpdata(): 11 | """ 12 | Dump data table models into filesystem. 13 | Only Root models are dumped 14 | 'Company', 'Contact', 'Deal', 15 | 'Sprint', 'Project', 'Organization','User' 16 | """ 17 | data_dir = app.config["DATA_DIR"] 18 | if not os.path.exists(data_dir): 19 | os.makedirs(data_dir) 20 | 21 | for model in RootModel.__subclasses__(): 22 | model_dir = os.path.abspath(os.path.join(data_dir, model.__name__)) 23 | if not os.path.exists(model_dir): 24 | os.mkdir(model_dir) 25 | 26 | for obj in model.query.all(): 27 | obj_as_str = str(obj).replace('/', '_') 28 | if len(obj_as_str) > 100: 29 | obj_as_str = obj_as_str[:100] 30 | 31 | record_path = os.path.abspath(os.path.join( 32 | model_dir, '%s_%s.json' % (obj.id, obj_as_str))) 33 | data = obj.as_dict() 34 | with open(record_path, 'w') as f: 35 | json.dump(data, f, indent=4, sort_keys=True) 36 | -------------------------------------------------------------------------------- /crm/cli/generate_graphql_docs.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen, PIPE 2 | 3 | from crm import app 4 | 5 | 6 | @app.cli.command() 7 | def generate_graphql_docs(): 8 | """ 9 | Generates schema.graphql IDL file and the GraphQL API documentation for queries and mutations. 10 | 11 | requires graphdoc to be installed. 12 | 13 | """ 14 | from crm import app 15 | sc = app.graphql_schema 16 | 17 | with open('./schema.graphql', "w") as f: 18 | f.write(str(sc)) 19 | 20 | p = Popen(['graphdoc', '--force', '-s', './schema.graphql', '-o', 21 | 'docs/graphqlapi'], stdout=PIPE, stderr=PIPE) 22 | 23 | p.communicate()[0] 24 | 25 | if p.returncode != 0: 26 | print("Failed to generate graphqlapi docs.") 27 | exit(1) 28 | -------------------------------------------------------------------------------- /crm/cli/import_data.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/crm/cli/import_data.py -------------------------------------------------------------------------------- /crm/cli/load.py: -------------------------------------------------------------------------------- 1 | from crm import app 2 | from crm.apps.contact.models import SubgroupName, Subgroup, ActivityType, Activity 3 | from crm.apps.currency.models import Currency 4 | from crm.db import db 5 | from crm.apps.country.countries import CountriesEnum 6 | from crm.apps.country.models import Country 7 | 8 | 9 | @app.cli.command() 10 | def load(): 11 | """ 12 | Add missing enum data to tables that use these enums 13 | i.e contact.countries, contact.subgroups 14 | """ 15 | for country in CountriesEnum: 16 | c = Country.query.filter_by(name=country).first() 17 | if c is not None: 18 | continue 19 | c = Country(name=country) 20 | db.session.add(c) 21 | 22 | for subgroup in SubgroupName: 23 | s = Subgroup.query.filter_by(groupname=subgroup).first() 24 | if s is not None: 25 | continue 26 | s = Subgroup(groupname=subgroup) 27 | db.session.add(s) 28 | 29 | for activity in ActivityType: 30 | a = Activity.query.filter_by(type=activity).first() 31 | if a is not None: 32 | continue 33 | a = Activity(type=activity) 34 | db.session.add(a) 35 | 36 | for currency in ['USD', 'EUR', 'AED', 'GBP', 'BTC']: 37 | c = Currency.query.filter_by(name=currency).first() 38 | if c is not None: 39 | continue 40 | c = Currency(name=currency) 41 | db.session.add(c) 42 | 43 | db.session.commit() -------------------------------------------------------------------------------- /crm/cli/loadfixtures.py: -------------------------------------------------------------------------------- 1 | from crm import app 2 | from crm.fixtures import generate_fixtures 3 | 4 | 5 | @app.cli.command() 6 | def loadfixtures(): 7 | """ 8 | populate DB with Test/Random Data 9 | """ 10 | generate_fixtures() 11 | -------------------------------------------------------------------------------- /crm/cli/rq_worker.py: -------------------------------------------------------------------------------- 1 | from rq import Queue, Connection, Worker 2 | from crm import app 3 | from crm.rq import conn, redis_url 4 | 5 | 6 | @app.cli.command() 7 | def rq_worker(): 8 | listen = ['default'] 9 | 10 | print('Connecting to redis on %s' % redis_url) 11 | with Connection(conn): 12 | worker = Worker(list(map(Queue, listen))) 13 | worker.work() -------------------------------------------------------------------------------- /crm/cli/syncdata.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | import subprocess 4 | 5 | from crm import app 6 | from crm.cli.dumpcache import _dump 7 | 8 | 9 | def _push(number_commits): 10 | print('\n\t\t\033[94mPUSHING %d COMMITS(s) TO REMOTE REPO\033[0m\n' % number_commits) 11 | data_dir = app.config["DATA_DIR"] 12 | dot_git = os.path.abspath(os.path.join(data_dir, '.git')) 13 | 14 | p = subprocess.Popen( 15 | [ 16 | 'git', 17 | '--git-dir=%s' % dot_git, 18 | '--work-tree=%s' % os.path.abspath(data_dir), 19 | 'push' 20 | ] 21 | ) 22 | 23 | out1, out2 = p.communicate() 24 | 25 | if not p.returncode == 0: 26 | print('Error committing files to git') 27 | print(out1, out2) 28 | print('\n\t\t\033[92mPUSH SUCCESSFUL\033[0m\n') 29 | 30 | 31 | @app.cli.command() 32 | def syncdata(): 33 | """ 34 | Sync Cached DB changes in Redis to file system (DATA_DIR) 35 | Push changes. 36 | """ 37 | while True: 38 | dumped_keys_length = _dump() 39 | if dumped_keys_length > 0: 40 | _push(dumped_keys_length) 41 | print('\t\tWAITING') 42 | time.sleep(5) 43 | -------------------------------------------------------------------------------- /crm/cli/update_currency.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | import requests 4 | 5 | from crm import app 6 | from crm.apps.currency.models import Currency 7 | from crm.db import db 8 | 9 | session = requests.Session() 10 | 11 | 12 | @app.cli.command() 13 | def update_currency_rates(): 14 | """ 15 | Updates Currencies exchange rates to USD 16 | """ 17 | while True: 18 | res = session.get('http://apilayer.net/api/live?access_key=955ebff5d2c404fcfb383b587a02a97b¤cies=AED,GBP,EUR,BTC&format=1') 19 | if res.status_code != 200: 20 | print('error getting currency rates : ', res.content) 21 | time.sleep(21600) 22 | continue 23 | 24 | res = res.json()['quotes'] 25 | AED = round(1.0/res['USDAED'], 2) 26 | GBP = round(1.0/res['USDGBP'], 2) 27 | EUR = round(1.0/res['USDEUR'], 2) 28 | BTC = round(1.0/res['USDBTC'], 2) 29 | 30 | 31 | Currency.query.filter_by(name='AED').update({'value_usd': AED}) 32 | Currency.query.filter_by(name='GBP').update({'value_usd': GBP}) 33 | Currency.query.filter_by(name='EUR').update({'value_usd': EUR}) 34 | Currency.query.filter_by(name='BTC').update({'value_usd': BTC}) 35 | print('Last update : ', datetime.datetime.now()) 36 | db.session.commit() 37 | print('Now sleeping for 6 hours') 38 | time.sleep(21600) 39 | -------------------------------------------------------------------------------- /crm/events/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | THIS PACKAGE IS THE PLACE FOR SQLALCHEMY EVENT HANDLERS 4 | 5 | """ 6 | 7 | # Import and expose all modules in this package. 8 | # Now any added module/command will be exposed automatically 9 | 10 | import pkgutil 11 | import inspect 12 | 13 | 14 | __all__ = [] 15 | 16 | for loader, name, is_pkg in pkgutil.walk_packages(__path__): 17 | module = loader.find_module(name).load_module(name) 18 | 19 | for name, value in inspect.getmembers(module): 20 | if name.startswith('__'): 21 | continue 22 | 23 | globals()[name] = value 24 | __all__.append(name) 25 | -------------------------------------------------------------------------------- /crm/events/catch_db_updates.py: -------------------------------------------------------------------------------- 1 | # from sqlalchemy.event import listen 2 | # 3 | # from crm.db import db 4 | # 5 | # 6 | # def catch_db_updates_after_flush(db_session, flush_context): 7 | # """ 8 | # WARNING 9 | # ---------------------------------------------------------- 10 | # THIS FUNCTION IS CALLED SEVERAL TIMES. 11 | # WHATEVER IS PUT HERE SHOULD BE IDEMPOTENT OPERATIONS ONLY 12 | # ---------------------------------------------------------- 13 | # 14 | # 15 | # 16 | # :param db_session: DB session 17 | # :param flush_context: Internal UOWTransaction object which handles the details of the flush. 18 | # """ 19 | # 20 | # # db_session.dirty, db_session.created, db_session.new 21 | # # are vanished during (after_commit) & (after_transaction_end) events 22 | # # we cache these values in db_session.info['changes'] so we can process them later 23 | # # after data is written to DB 24 | # 25 | # # at this point of time we can't call model.as_dict() 26 | # # and cache this value, because object is not saved in db yet 27 | # # so we save it in temp location db_session.info['changes'] 28 | # # we catch these cached values on another event (after transaction completes) 29 | # # we process them there 30 | # 31 | # db_session.info['changes'] = { 32 | # 'updated': db_session.dirty, 33 | # 'created': db_session.new, 34 | # 'deleted': db_session.deleted 35 | # } 36 | # 37 | # listen(db.session, 'after_flush', catch_db_updates_after_flush) 38 | -------------------------------------------------------------------------------- /crm/events/notify_new_task.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sqlalchemy import event 3 | from flask import request 4 | 5 | from crm.apps.message.models import Message 6 | from crm.apps.task.models import Task 7 | 8 | 9 | @event.listens_for(Task, 'after_insert') 10 | def receive_after_insert(mapper, connection, task): 11 | if request: 12 | url_root = request.url_root.strip('/') 13 | else: 14 | host = os.getenv('DOMAIN') 15 | if not host: 16 | print('Missing DOMAIN env variable. emails are not going to be sent') 17 | return 18 | url_root = 'https://{}'.format(host) 19 | 20 | if task.assignee: 21 | msg = Message( 22 | title='You have a new assigned task', 23 | content='Title: {}
Description:{}

Task Link: {}'.format(task.title, task.description, url_root + task.admin_view_link(), task), 24 | user=task.assignee, 25 | task=task, 26 | forced_destinations=','.join([e.email for e in task.assignee.emails]) 27 | ) 28 | 29 | message_tbl = Message.__table__ 30 | # Create a message for assignee only that he/she has new task 31 | connection.execute( 32 | message_tbl.insert(). 33 | values( 34 | id=msg.uid, 35 | title=msg.title, 36 | content=msg.content, 37 | user_id=msg.user.id 38 | ) 39 | ) 40 | 41 | 42 | @event.listens_for(Task, 'after_update') 43 | def receive_after_update(mapper, connection, task): 44 | if request: 45 | url_root = request.url_root.strip('/') 46 | else: 47 | host = os.getenv('DOMAIN') 48 | if not host: 49 | print('Missing DOMAIN env variable. emails are not going to be sent') 50 | return 51 | url_root = 'https://{}'.format(host) 52 | 53 | if task.assignee: 54 | msg = Message( 55 | title='Your assigned task has been updated', 56 | content='Title: {}
Description:{}

Task Link: {}'.format(task.title, task.description, url_root + task.admin_view_link(), task), 57 | user=task.assignee, 58 | task=task, 59 | forced_destinations=','.join([e.email for e in task.assignee.emails]) 60 | ) 61 | 62 | message_tbl = Message.__table__ 63 | # Create a message for assignee only that he/she has new task 64 | connection.execute( 65 | message_tbl.insert(). 66 | values( 67 | id=msg.uid, 68 | title=msg.title, 69 | content=msg.content, 70 | user_id=msg.user.id 71 | ) 72 | ) 73 | -------------------------------------------------------------------------------- /crm/events/update_auto_fields.py: -------------------------------------------------------------------------------- 1 | from crm.db import db 2 | 3 | from sqlalchemy.event import listen 4 | 5 | 6 | def update_auto_fields_before_flush(db_session, flush_context, instances): 7 | """ 8 | WARNING 9 | ---------------------------------------------------------- 10 | THIS FUNCTION IS CALLED SEVERAL TIMES. 11 | WHATEVER IS PUT HERE SHOULD BE IDEMPOTENT OPERATIONS ONLY 12 | ---------------------------------------------------------- 13 | 14 | This is called after u do session.add() before any operation on objects 15 | It's the right place to manipulate objects we need to alter before trying 16 | to Hit DB 17 | 18 | so we update (id) field and original author here 19 | we update last author on updated objects 20 | using model.update_auto_fields() which is idempotent 21 | 22 | :param db_session: DB session 23 | :param flush_context: Internal UOWTransaction object which handles the details of the flush. 24 | :param instances: affected instances, but usually holds value of None 25 | """ 26 | 27 | # Update ID and original_author 28 | # object.update_auto_fields() of course is aware of saved object ids but not newly created ids 29 | # We keep track of all ids generated here and guarantee uniqueness for each model 30 | 31 | ids = {} 32 | 33 | for created in db_session.new: 34 | cls_name = created.__class__.__name__ 35 | ids[cls_name] = ids.get(cls_name, []) 36 | 37 | while True: 38 | created.update_auto_fields() 39 | if created.id not in ids[cls_name]: 40 | ids[cls_name].append(created.id) 41 | break 42 | else: 43 | # Forcing created.id = None makes created.update_auto_fields() generates new id 44 | # sinc obj.uid returns id if set, but if None it generates another uid 45 | created.id = None 46 | 47 | # Update last modifier 48 | for updated in db_session.dirty: 49 | if not db_session.is_modified(updated): 50 | continue 51 | updated.update_auto_fields(update=True) 52 | 53 | listen(db.session, 'before_flush', update_auto_fields_before_flush) 54 | -------------------------------------------------------------------------------- /crm/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Import and expose all modules in this package. 3 | Now any added module/command will be exposed automatically 4 | """ 5 | 6 | import pkgutil 7 | import inspect 8 | import os 9 | 10 | __all__ = [] 11 | 12 | for loader, name, is_pkg in pkgutil.walk_packages(__path__): 13 | excluded_middlewares = [md.strip() for md in os.getenv('EXCLUDED_MIDDLEWARES', '').split(',')] 14 | if name in excluded_middlewares: 15 | continue 16 | 17 | module = loader.find_module(name).load_module(name) 18 | 19 | for name, value in inspect.getmembers(module): 20 | if name.startswith('__'): 21 | continue 22 | 23 | globals()[name] = value 24 | __all__.append(name) 25 | -------------------------------------------------------------------------------- /crm/rq.py: -------------------------------------------------------------------------------- 1 | import os 2 | import redis 3 | from rq import Queue 4 | 5 | redis_url = os.getenv('CACHE_BACKEND_URI') 6 | if redis_url: 7 | redis_url = redis_url[:-2] + '/1' 8 | else: 9 | redis_url = 'redis://localhost:6379/1' 10 | 11 | conn = redis.from_url(redis_url) 12 | 13 | queue = Queue(connection=conn) 14 | -------------------------------------------------------------------------------- /crm/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from importlib import import_module 3 | from os.path import dirname 4 | 5 | LOGGING_CONF = { 6 | 'version': 1, 7 | 'formatters': {'default': { 8 | 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', 9 | }}, 10 | 'handlers': {'wsgi': { 11 | 'class': 'logging.StreamHandler', 12 | 'formatter': 'default' 13 | }}, 14 | 'root': { 15 | 'level': 'INFO', 16 | 'handlers': ['wsgi'] 17 | } 18 | } 19 | 20 | 21 | # ../crm. 22 | STATIC_DIR = os.path.abspath( 23 | os.path.join( 24 | dirname(dirname(__file__)), 25 | 'static' 26 | ) 27 | ) 28 | 29 | STATIC_URL_PATH = "/" + os.path.relpath(STATIC_DIR) 30 | 31 | IMAGES_DIR = os.path.join(STATIC_DIR, "uploads", "images") 32 | 33 | ATTACHMENTS_DIR = os.path.join(STATIC_DIR, "uploads", "attachments") 34 | 35 | DATA_DIR = os.getenv('DATA_DIR') 36 | 37 | CACHE_BACKEND_URI = os.getenv('CACHE_BACKEND_URI', "http://127.0.0.1:6379") 38 | 39 | SENDGRID_API_KEY = os.getenv('SENDGRID_API_KEY') 40 | 41 | SUPPORT_EMAIL = os.getenv('SUPPORT_EMAIL') 42 | 43 | SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI') 44 | 45 | ###################### 46 | # Leave as the last line 47 | ######################## 48 | 49 | # Load env settings into globals 50 | settings_module = 'crm.settings_%s' % os.getenv("ENV", 'dev') 51 | env_settings = import_module(settings_module).__dict__ 52 | globals().update(env_settings) 53 | -------------------------------------------------------------------------------- /crm/settings_dev.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEBUG = True 4 | Testing = True 5 | 6 | DB_PATH = os.path.abspath( 7 | os.path.join( 8 | os.path.dirname(os.path.dirname(__file__)), 9 | 'dev.db' 10 | ) 11 | ) 12 | 13 | SQLALCHEMY_DATABASE_URI = os.getenv( 14 | 'SQLALCHEMY_DATABASE_URI', 15 | 'sqlite:///%s' % DB_PATH 16 | ) 17 | 18 | SQLALCHEMY_TRACK_MODIFICATIONS = True 19 | SQLALCHEMY_ECHO = False 20 | SQLALCHEMY_RECORD_QUERIES = True 21 | 22 | CACHE_BACKEND_URI = 'memory://' 23 | 24 | DATA_DIR = 'data' 25 | 26 | SUPPORT_EMAIL = os.getenv('SUPPORT_EMAIL', None) 27 | SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY", None) 28 | -------------------------------------------------------------------------------- /crm/settings_prod.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEBUG = False 4 | 5 | 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | 8 | DATA_DIR = os.getenv('DATA_DIR', '/opt/code/github/incubaid/data_crm') 9 | -------------------------------------------------------------------------------- /crm/settings_test.py: -------------------------------------------------------------------------------- 1 | from .settings_dev import * -------------------------------------------------------------------------------- /docs/AddNewRequirement.md: -------------------------------------------------------------------------------- 1 | ## How to add new dependency 2 | 3 | **System requirements** 4 | 5 | - Linux/Ubuntu : add it to `requiremtns.apt` 6 | - Mac : add it to `requiremtns.brew` 7 | 8 | **Python package** 9 | 10 | - Add to `requiremtns.pip` & `requiremtns-testing.pip` using the following procedure 11 | - Create and activate new virtualenv `virtualenv -p python3 my_env` && `. my_env/bin/activate` 12 | - Install old requirements `pip install -r requiremtns.pip` 13 | - Save installed package in a file using `pip freeze > file1.txt` or using a tool like [pepreqs](https://github.com/bndr/pipreqs) 14 | - Install new packag(s) 15 | - Save installed package in a file using `pip freeze > file2.txt` or using a tool like [pepreqs](https://github.com/bndr/pipreqs) 16 | - Get difference between 2 files `diff file1.txt file2.txt` 17 | - Add difference package to `requirements.pip` & `requirements-testing.pip` 18 | 19 | **Nodejs package** 20 | - Add to `requirements.npm` 21 | 22 | -------------------------------------------------------------------------------- /docs/AuthenticationMiddleware.md: -------------------------------------------------------------------------------- 1 | ## Authentication 2 | - We don't support authentication on CRM level unfortuantely, we depend on 3rd party [oauth](https://oauth.net/) services 3 | like [IYO](https://itsyou.online) to do so 4 | - If you're going to implement your own authentication mechanism, then you need a [Middleware](https://en.wikipedia.org/wiki/Middleware) to do so 5 | - you can [Define your own middlewares](Middlewares.md) in `crm.middlewares` package 6 | - By default in production mode we have [IYO](https://itsyou.online) authentication support using this 2 steps process: 7 | - we use [Caddy Server](https://caddyserver.com/) with [IYO](https://itsyou.online) to do redirection and authentication for us 8 | and put a JWT token in request header 9 | 10 | - We use a [Middleware](https://en.wikipedia.org/wiki/Middleware) `crm.middlewares.iyo` to : 11 | - Validate the JWT token, extract user info out of it 12 | - If user not in `users` table, we create a new user, otherwise we update user info 13 | - We set session['user'] entry for that user, so later if we found info in session 14 | we authorize user directly without hitting the Database 15 | - If you want to run the app without `crm.middlewares.iyo` then before running the app you may do `export EXCLUDED_MIDDLEWARES=iyo` 16 | - All session data are invalidated when you restart the CRM app 17 | > This done by generating new `app.secret_key` settings for CRM app every time is started 18 | 19 | ###### Writing your own Authentication middleware 20 | 21 | - Put a new middleware in `crm.middlewares` package 22 | - You can get current flask session and request from `flask.session` and`flask.request` 23 | - write a middleare function decorated by `@app.before_request` and hamdle auth lofic there 24 | ```python 25 | from crm import app 26 | from flask import session, request 27 | 28 | @app.before_request 29 | def my_middleware(): 30 | pass 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/Caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | - We use [Flask Cache](https://pythonhosted.org/Flask-Cache/) 4 | - For now We've 2 Types of supported cache backends 5 | - Memory 6 | - [Redis](https://redis.io/) 7 | 8 | > we can always add more backends easily since [Flask Cache](https://pythonhosted.org/Flask-Cache/) supports many other backends 9 | 10 | - In development mode `memory` cache is used unless you explicitly do on of the following 11 | - Set `CACHE_BACKEND_URI` environment variable i.e `export CACHE_BACKEND_URI=redis://{ip}:{port}/{db_number}` 12 | - Changed the CACHE_BACKEND_URI setting in `crm.settings_dev.py` explicitly to refer to a[Redis](https://redis.io/) URL 13 | 14 | - In production mode, You **must use `Redis`** by executing : ``export CACHE_BACKEND_URI=redis://{ip}:{port}/{db_number}`` 15 | 16 | **Problems with Memory Cache** 17 | 18 | All will work fine but some commands like `flask dumpcache` won't work. 19 | becasue cached memory in CRM app can't be accessed by another process run by another command `flask dumpcache` 20 | If you're testing `flask dumpcache` or similar command, consider use a [Redis](https://redis.io/) cache 21 | -------------------------------------------------------------------------------- /docs/Configuration.md: -------------------------------------------------------------------------------- 1 | ## Configuration 2 | 3 | ### Settings files 4 | 5 | ###### Shared settings between all environments 6 | 7 | goes inside `crm.settings.py` 8 | 9 | - `STATIC_DIR` static files directory 10 | - `STATIC_URL_PATH` static files URL prefix 11 | - `IMAGES_DIR` where images uploads goes 12 | - `LOGGING_CONF` logging configuration 13 | - `ATTACHMENTS_DIR` Attachments directory for the [Mailin Feature](MailinMailout.md) 14 | ###### Development mode settings 15 | 16 | goes inside ```crm.settings_dev.py``` 17 | 18 | - `DEBUG = True` Enable debugging 19 | - `Testing = True` Enable testing mode 20 | - [SQLALCHEMY](https://www.sqlalchemy.org/) debugging options 21 | - `SQLALCHEMY_TRACK_MODIFICATIONS = True` 22 | - `SQLALCHEMY_ECHO = True` 23 | - `SQLALCHEMY_RECORD_QUERIES = True` 24 | 25 | 26 | ###### Production mode settings 27 | 28 | goes inside ```crm.settings_prod.py``` 29 | 30 | - `DEBUG = FALSE` Disable debugging 31 | - [SQLALCHEMY](https://www.sqlalchemy.org/) debugging options 32 | - `SQLALCHEMY_TRACK_MODIFICATIONS = False` 33 | - `SQLALCHEMY_ECHO=False` & `SQLALCHEMY_RECORD_QUERIES=False` no need to add them they're set to `False` by default 34 | 35 | 36 | ###### Environment variables settings 37 | - `export CACHE_BACKEND_URI=redis://{ip}:{port}/{db_number}` to set a [redis](https://redis.io/) cache backend 38 | > In **production mode**, must be set explicitly to a [redis](https://redis.io/) URL. 39 | 40 | > In **development mode**, Not required to set, but the default Cache will be **memory** 41 | 42 | > In **development mode** command `flask dumpcache` won't work if Cache backend is **memory** 43 | 44 | > Memory cache in a CRM app process can't be accessed by another process like `flask dumpcache` 45 | so if you want to use `flask dumpcache` you have to use shared cache like [redis](https://redis.io/) 46 | 47 | - `export SQLALCHEMY_DATABASE_URI=postgresql://{user}:{pass}@{ip}:{port}/{db_name}` Use [Postgresql](https://www.postgresql.org/) db 48 | > In **production mode**, must be set explicitly to an [RDBMS](https://en.wikipedia.org/wiki/Relational_database_management_system) like [Postgres](https://www.postgresql.org/) 49 | 50 | > In **development mode** Not required but default is to use a [Sqlite](https://www.sqlite.org/) db called `db.dev` in root dir 51 | 52 | - `export EXCLUDED_MIDDLEWARES=iyo,mw2,mw3` Quickly disable a comma separated list of middlewares 53 | 54 | > In **development mode only** if you want to disable some modules during development like [IYO](https://itsyou.online) authentication middleware 55 | You just have to provide comma separated list of middleware module names 56 | 57 | - `export DATA_DIR={path}` change the path where `flask dumpdata` exports DB data to and `flask loaddata` load data into DB from 58 | > In **production mode** is set by default to `/opt/code/github/incubaid/data_crm` 59 | 60 | > In **development mode** is set by default to `data` dir under the root directory 61 | 62 | - export `SENDGRID_API_KEY` for [Mail In/Out](MailinMailOut.md). 63 | -------------------------------------------------------------------------------- /docs/DBEvents.md: -------------------------------------------------------------------------------- 1 | ## DB Events 2 | 3 | - DB Events are functions/callbacks to be executed on responding to some action like Model update/delete/create 4 | - More info on this topic and list of all supported [SQLALCHEMY Events](- List of all [SqlAlchemy events](http://docs.sqlalchemy.org/en/latest/orm/events.html)) 5 | 6 | **Registering a new hook/event** 7 | 8 | - All events are added in package `crm.events` 9 | - Add a new module there with a function call back to be called when some event happen 10 | - Register your function call back by binding it to some event 11 | 12 | - Example: `crm.events.catch_db_updates.py` 13 | 14 | ```python 15 | from sqlalchemy.event import listen 16 | 17 | from crm.db import db 18 | 19 | 20 | def catch_db_updates_after_flush(db_session, flush_context): 21 | 22 | db_session.info['changes'] = { 23 | 'updated': db_session.dirty, 24 | 'created': db_session.new, 25 | 'deleted': db_session.deleted 26 | } 27 | 28 | listen(db.session, 'after_flush', catch_db_updates_after_flush) 29 | 30 | ``` 31 | 32 | - **db.session.new** contains all newly created objects but, *No many to many models are included* 33 | - **db.session.dirty** contains all updated objects 34 | - **db.session.deleted** contains list of deleted objects 35 | 36 | 37 | **WARNING** 38 | - DB Events are disabled when you execute command ```flask loaddata``` because usually you don't want 39 | some actions that may manipulate your data to be executed when you try to load a data mirror in physical DB 40 | 41 | - Most Events won't work with bulk updates and bulk inserts unfortunately 42 | 43 | ## Explanation of existing DB events handlers in CRM 44 | 45 | **`crm.events.update_auto_fields.py`** 46 | 47 | - registers an `after_flush` event callback which gets newly created non many to many models and assign them a unique ID string 48 | and add original author id to model. Also add latest author user id to model 49 | 50 | **`crm.events.catch_db_updates.py`** 51 | 52 | - registers an `after_flush` event callback which gets values of `db.session.new`, `db.session.dirty` and `db.session.deleted` and cache them 53 | in `db.session.info['changes']` because we need to access these data when the event `after_transaction` is triggered 54 | but during `after_transaction` we have a problem that `db.session.new`, `db.session.dirty` and `db.session.deleted` are all empty, so we cache these data 55 | in temp location where we can access them in the appropriate time 56 | 57 | 58 | **`crm.events.cache_db_updates.py`** 59 | 60 | - registers an `after_transaction` event callback which gets all db updates saved in `db.session.info['changes']` 61 | and adds them to the existing cache backend. so we can keep track of any change to DB in memory and we can dump it into JSON 62 | files in `DATA_DIR` automatically 63 | -------------------------------------------------------------------------------- /docs/GraphqlQueryLanguage.md: -------------------------------------------------------------------------------- 1 | ### Query Language 2 | We use our own query language to query Data in CRM 3 | In any query, you define a field name and its value as a query string i.e `contacts({'firstname': 'my-query'}){..}` 4 | 5 | #### Query String structure 6 | 7 | - Exact match `=` 8 | - Use string you want to match directly i.e `contacts({'firstname': 'Jack'}){..}` 9 | - Not equals `!=` 10 | - put `~` before your string i.e `contacts({'firstname': '~Jack'}){..}` 11 | - contains 12 | - `contacts({'firstname': 'contains(ali)'}){..}` 13 | - like 14 | - `contacts({'firstname': 'contains(ali%)'}){..}` 15 | - null 16 | - `contacts({'lastname': 'null'}){..}` 17 | - not null 18 | - `contacts({'lastname': '~null'}){..}` 19 | - in 20 | - `contacts({'lastname': 'in(ali, fathy)'}){..}` 21 | - not in 22 | - `contacts({'lastname': '~in(ali, fathy)'}){..}` 23 | - `>,<,>=,<=` 24 | - `deals({'value': '>=(10)'}){..}` or `deals({'value': '>=10'}){..}` 25 | - `deals({'created_at': '>=(1999-02-02)'}){..}` 26 | - `deals({'created_at': '<=1999-02-02'}){..}` 27 | - ranges 28 | - `deals({'value': '[4, 10]'}){..}` means `4 <= value <= 10` 29 | - `deals({'value': ']4, 10['}){..}` means `4 < value < 10` 30 | - `deals({'value': '[4, 10['}){..}` means `4 <= value < 10` 31 | - `deals({'value': ']4, 10]'}){..}` means `4 < value <= 10` 32 | - `or` 33 | - `or(ali,fathy)` 34 | - `or(contains(ali), contains(fathy))` 35 | - `and` 36 | - `and(contains(ali), ~alii)` 37 | -------------------------------------------------------------------------------- /docs/Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | - CRM is a [Flask](flask.pocoo.org/) app 4 | - We have 2 main modes to install and run the application ```Development``` & ```Production``` modes 5 | Depending on the mode you're running requirements may change (a bit) not dramatically 6 | 7 | ### Prepare Your virtual environment 8 | 9 | - ```virtualenv -p python3 crm_env``` 10 | - ```. crm_env/bin/activate``` 11 | 12 | 13 | ### Install required packages 14 | 15 | ###### Automatically 16 | 17 | - Use the script ```./prepare.sh``` to install all dependencies 18 | - usage: 19 | ``` 20 | ./prepare --prod (Install Production dependencies) 21 | ./prepare --dev (Install Development dependencies) 22 | ``` 23 | 24 | ###### Manually 25 | 26 | - Install System level dependencies 27 | - Ubuntu ```apt-get -y < requirements.apt``` 28 | - Mac ```xargs brew install < requirements.brew``` 29 | 30 | - Install Python level dependencies 31 | - Development environment ```pip3 install -r requirements-testing.pip``` 32 | - Production environment ```pip3 install -r requirements.pip``` 33 | 34 | - Install Nodejs level dependencies 35 | - Development environment ```cat requirements.npm | sudo xargs npm install -g``` 36 | - Production environment ```Nothing yet``` 37 | 38 | ### Notes 39 | 40 | - Development mode python packages that are not needed in production: 41 | - ```flask-shell-ipython``` 42 | - ```coverage``` 43 | - ```nose``` 44 | - ```ipdb``` 45 | 46 | - Development nodejs packages that are not needed in production: 47 | - ```@2fd/graphdoc``` It's used to re-create graphql API docs from a [graphql](http://graphql.org/learn/) schema file and it's the only 48 | nodejs dependency for now 49 | -------------------------------------------------------------------------------- /docs/Middlewares.md: -------------------------------------------------------------------------------- 1 | ## Middlewares 2 | 3 | **What is a middleware** 4 | 5 | - A middleware in a web application is a piece of software that runs 6 | before any request in the web application and thus it can do operations 7 | that is global to all/some http actions in your app. 8 | Check [Wikipedia Middleware](https://en.wikipedia.org/wiki/Middleware) for more info 9 | 10 | **How to add a middleware in CRM system** 11 | 12 | - Just add your middleware module inside the package `crm.middlewares` 13 | - write your middleware logic in a function decorated by `@app.before_first_request` and import `flask.request` to get current request 14 | then middleware is exposed automatically 15 | ```python 16 | from crm import app 17 | from flask import request 18 | 19 | @app.before_request 20 | def my_middleware(): 21 | pass 22 | ``` 23 | 24 | **Example** 25 | 26 | CRM authentication is done through a middleware `crm.middlewares.iyo`, please check [Authentication middleware](AuthenticationMiddleware.md) 27 | 28 | 29 | **How to disable a middleware during development mode** 30 | 31 | Before running app, do `export EXCLUDED_MIDDLEWARES=md1,md2,..` where `EXCLUDED_MIDDLEWARES` takes comma separated list of middleware modulenames in `crm.middlewares` package 32 | -------------------------------------------------------------------------------- /docs/Models.md: -------------------------------------------------------------------------------- 1 | ## Models 2 | 3 | - All models **Must** be defined under `crm.apps.{my_sub_app}.models` an example is `crm.apps.user.models` 4 | - ِAll models inherit from `db.Model` 5 | 6 | > There's difference between (Many To Many) models and non (Many To Many) models 7 | And we need a way to differentiate between both especially when we export/import DB data 8 | So each category has different Parent class.That says `flask dumpdata` & `flask loaddata` commands 9 | will not work correctly if you're not inheriting from proper parent while defining your model 10 | 11 | - **Many To Many** 12 | 13 | - Must inherit from `crm.db.ManyToManyBaseModel)` 14 | - Primary Key (id) field is integer and auto incremental 15 | - Usually records in these tables are auto created by Flask ORM [SqlAlchemy](https://www.sqlalchemy.org/) 16 | That's why we don't want to alter their creation mechanism. 17 | 18 | ```python 19 | class ContactSubgroup(db.Model, ManyToManyBaseModel): 20 | __tablename__ = "contacts_subgroups" 21 | 22 | subgroup_id = db.Column( 23 | db.String(5), 24 | db.ForeignKey('subgroups.id') 25 | ) 26 | 27 | contact_id = db.Column( 28 | db.String(5), 29 | db.ForeignKey("contacts.id") 30 | ) 31 | ``` 32 | 33 | 34 | - **(Non Many to Many)** 35 | 36 | - Must inherit from `crm.db.BaseModel` 37 | - Primary Key (id) is string of unique 5 characters and assigned by an ORM [SqlAlchemy Event](http://docs.sqlalchemy.org/en/latest/orm/events.html) 38 | > `crm.events.update_auto_fields` registers a `before_flush` event to update id field with a unique ID string 39 | This event finds newly created records and alter their IDS with random ones. 40 | 41 | > Note that newly created tables list in a DB `session.new` doesn't contain any many to many field 42 | so we're sure that only non Many to Many fields are affected 43 | 44 | ``` 45 | class Contact(db.Model, BaseModel, RootModel): 46 | 47 | __tablename__ = "contacts" 48 | 49 | firstname = db.Column( 50 | db.String(255), 51 | nullable=False, 52 | index=True 53 | ) 54 | 55 | lastname = db.Column( 56 | db.String(255), 57 | default="", 58 | index=True 59 | ) 60 | description = db.Column( 61 | db.Text() 62 | ) 63 | ``` 64 | 65 | 66 | - **Root models** 67 | > We've 7 Root models, 68 | - Company 69 | - Contact 70 | - Deal 71 | - Sprint 72 | - Project 73 | - Organization 74 | - User 75 | 76 | - Root models are these that is exported during `flask dumpdata` command 77 | - A Root model must inherit from `crm.db.RootModel` 78 | - Since only Root models are exported during `flask dumpdata`, all info related to a record from any other non Root 79 | model is contained inside Root model data, that says exported data has duplications but more readable 80 | -------------------------------------------------------------------------------- /docs/Philosophy.md: -------------------------------------------------------------------------------- 1 | ## Philopsophy 2 | 3 | #### why do we need our own CRM system 4 | 5 | - Managing ITO (Internal Token Offerings) for [Threffoldtoken](https://threefoldtoken.com/login/) 6 | - Easy Authentication & Authorization through [IYO](https://itsyou.online) 7 | - Openness 8 | - All Data should be imported/exported from/into JSON format easily 9 | - Any Data change should be visible instantly in a [Git](https://git-scm.com/) Repo 10 | -------------------------------------------------------------------------------- /docs/PrefabInstallation.md: -------------------------------------------------------------------------------- 1 | ## Prefab Installation 2 | 3 | You don't need to worry about this section if you're not familiar with [Jumpscale](https://github.com/Jumpscale) 4 | 5 | **Install Bash tools** 6 | 7 | 8 | ``` 9 | curl https://raw.githubusercontent.com/Jumpscale/bash/master/install.sh?$RANDOM > /tmp/install.sh;bash /tmp/install.sh 10 | source ~/.bash_profile 11 | ``` 12 | 13 | **Build Docker CRM Image** 14 | 15 | ``` 16 | ZInstall_crm -p 443 -u "crm.threefoldtoken.com" -o threefold.crm_users -s UrzXJ81iWdckOoOcsBD4Xo9-6ThaMpfOC5sW8apUYwad -i production_crm -e hamdya@greenitglobe.com 17 | ``` 18 | 19 | **Build Docker CRM Container** 20 | 21 | ``` 22 | ZInstall_crm -p 443 -i production_crm 23 | ZDockerActive -b "jumpscale/js9_docgenerator" -a "-p 443:443 -p 80:80" -c "ZInstall_docgenerator" -i crm_production 24 | ``` 25 | 26 | **Access Docker CRM Container** 27 | ``` 28 | docker exec -ti crm bash 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /docs/Production.md: -------------------------------------------------------------------------------- 1 | # Update Production procedures 2 | 3 | - Deploy from ```production``` branch 4 | - After any change to our [graphql](graphql.org/learn/) api, regenerate [graphql](graphql.org/learn/) api usig ```generate_graphql_docs``` then commit new docs 5 | 6 | 7 | - **Postgres** 8 | - Backup postgres database or ensure a recent version of postgres backup 9 | 10 | - **Caddy** 11 | - Update ```caddy``` config with the latest config if any 12 | - stop and start caddy with new config 13 | ``` 14 | echo **START**;ulimit -n 8192; /opt/bin/caddy -conf=/opt/cfg/caddy.cfg -agree && echo **OK** || echo **ERROR** 15 | ``` 16 | - **CRM** 17 | - check where is ```DATA_DIR``` in ```crm.settings_prod.py``` and backup this DIR 18 | - **`Warning`** ```DATA_DIR``` settings may have been overridden by `export DATA_DIR={custom_path}` 19 | 20 | - make sure you're in production branch and pull new production code 21 | - stop crm 22 | - backup data by running ```flask dumpdata``` and take another copy `DATA_DIR` 23 | - runt script `./prepare.sh --prod` to install all needed requirements or refer to [Installation](Installation.md) page 24 | - run DB migrations ```flask db upgrade``` to run migrations and update Physical DB 25 | - make sure you can dumpdata correctly using ```flask dumpdata``` and take a 3rd copy of data for ```crm.settings.DATA_DIR``` 26 | - Run 27 | ``` 28 | /opt/code/github/incubaid/crm# echo **START**;cd /opt/code/github/incubaid/crm;export SQLALCHEMY_DATABASE_URI=postgresql://user:pass@host:5432/db;export CACHE_BACKEND_URI=redis://{ip}:{port}/{db_number};export ENV=prod;export FLASK_APP=app.py;flask db upgrade; uwsgi --ini uwsgi.ini && echo **OK** || echo **ERROR** 29 | 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/Structure.md: -------------------------------------------------------------------------------- 1 | ## Project Structure 2 | 3 | **Middlewares** 4 | - Under `crm.middlewares` 5 | - More info about [Middlewares](Middlewares.md) 6 | 7 | 8 | **Custom Command lines** 9 | - Under `crm.cli` 10 | - More info about [Commands](Commands.md) 11 | 12 | **Custom Events** 13 | - Under `crm.events` 14 | - More info about [DB Events](DBEvents.md) 15 | 16 | **Admin application** 17 | - Under `flask.apps.admin` 18 | - More info about [Admin app](AdminInterface.md) 19 | 20 | **Flask sub application** 21 | - Any package under `crm.apps` 22 | - Main modules for any sub app (except the [Admin app](AdminInterface.md)) are: 23 | - `models.py` for models definitions. Please follow [These conventions](Models.md) when defining your own models 24 | - `views.py` for HTTP end points definitions. . Please follow [These conventions](Views.md) when defining your own models 25 | - To add an HTTP end point, you put your code in a function inside `views.py` file in some app decorated by `@app.route` 26 | ``` 27 | @app.route('/api', methods=["POST"]) 28 | def api(): 29 | pass 30 | 31 | ``` 32 | - `graphql` package for [graphql](graphql.org/learn/) To expose `graphql` mutations & queries for this sub app. 33 | Please follow [These conventions](GraphqlAdvanced.md) 34 | - Put types in module `types.py` 35 | - Put arguments in module `arguments.py` 36 | - Put queries in module `queries.py` 37 | - Put mutations in module `mutations.py` 38 | 39 | 40 | **Main app** 41 | - [Flask](flask.pocoo.org/) Main entry point is ```app.py``` in the root directory 42 | - It imports actual [Flask](flask.pocoo.org/) app :: `crm.app` 43 | - `crm.app` is defined in `crm/__init__.py` and it's the actual [Flask](flask.pocoo.org/) app 44 | - `crm/__init__.py` contains code to initialize the app and registering all http endpoints for sub applications in 45 | `crm.apps` by importig their `views.py` module if found 46 | - It registers all custom commands by doing ```from crm.cli import *``` 47 | - It registers all middlewares by doing ```from crm.middlewares import *``` 48 | - It registers all db events by doing ```from crm.events import *``` 49 | -------------------------------------------------------------------------------- /docs/Views.md: -------------------------------------------------------------------------------- 1 | ## Views 2 | 3 | HTTP actions 4 | 5 | - surround an http action with ```@app.route('custom-relative-path')``` 6 | - Example: 7 | ```python 8 | from flask.templating import render_template 9 | 10 | from crm import app 11 | 12 | @app.route('/') 13 | def hello_world(): 14 | return render_template('home/index.html') 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/assets/addfilter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/addfilter.png -------------------------------------------------------------------------------- /docs/assets/createview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/createview.png -------------------------------------------------------------------------------- /docs/assets/detailsview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/detailsview.png -------------------------------------------------------------------------------- /docs/assets/editview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/editview.png -------------------------------------------------------------------------------- /docs/assets/extramenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/extramenu.png -------------------------------------------------------------------------------- /docs/assets/graphiql_docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/graphiql_docs.png -------------------------------------------------------------------------------- /docs/assets/graphql_many.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/graphql_many.png -------------------------------------------------------------------------------- /docs/assets/graphql_query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/graphql_query.png -------------------------------------------------------------------------------- /docs/assets/iyo-settings1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/iyo-settings1.png -------------------------------------------------------------------------------- /docs/assets/iyo-settings2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/iyo-settings2.png -------------------------------------------------------------------------------- /docs/assets/listview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/listview.png -------------------------------------------------------------------------------- /docs/assets/mainmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/assets/mainmenu.png -------------------------------------------------------------------------------- /docs/graphqlapi/assets/code.css: -------------------------------------------------------------------------------- 1 | .code { 2 | background-color: #f6f6f6; 3 | color: #4D4D4C; 4 | border: 1px solid #4D4D4C; 5 | font-size: 14px; 6 | font-family: 'Ubuntu Mono'; 7 | cursor: text; 8 | list-style-type: decimal; 9 | border-radius: 0.25rem; 10 | } 11 | 12 | .code .gutter { 13 | background: #f6f6f6; 14 | color: #4D4D4C; 15 | } 16 | 17 | .code .print-margin { 18 | width: 1px; 19 | background: #f6f6f6 20 | } 21 | 22 | .code li { 23 | min-height: 1em; 24 | background: #FFF; 25 | padding: 1px 8px; 26 | } 27 | .code li:hover { 28 | background: #EFEFEF; 29 | } 30 | 31 | .code .tab { 32 | padding-left: 2em; 33 | } 34 | 35 | .code .cursor { 36 | color: #AEAFAD 37 | } 38 | 39 | .code .marker-layer .selection { 40 | background: #D6D6D6 41 | } 42 | 43 | .code.multiselect .selection.start { 44 | box-shadow: 0 0 3px 0px #FFFFFF; 45 | } 46 | 47 | .code .marker-layer .step { 48 | background: rgb(255, 255, 0) 49 | } 50 | 51 | .code .marker-layer .bracket { 52 | margin: -1px 0 0 -1px; 53 | border: 1px solid #D1D1D1 54 | } 55 | 56 | .code .marker-layer .active-line { 57 | background: #EFEFEF 58 | } 59 | 60 | .code .gutter-active-line { 61 | background-color: #dcdcdc 62 | } 63 | 64 | .code .marker-layer .selected-word { 65 | border: 1px solid #D6D6D6 66 | } 67 | 68 | .code .invisible { 69 | color: #D1D1D1 70 | } 71 | 72 | .code .keyword, 73 | .code .meta, 74 | .code .storage, 75 | .code .storage.type, 76 | .code .support.type { 77 | color: #8959A8 78 | } 79 | 80 | .code .keyword.operator { 81 | color: #3E999F 82 | } 83 | 84 | .code .constant.character, 85 | .code .constant.language, 86 | .code .constant.numeric, 87 | .code .keyword.other.unit, 88 | .code .support.constant { 89 | color: #F5871F 90 | } 91 | 92 | .code .constant.other { 93 | color: #666969 94 | } 95 | 96 | .code .invalid { 97 | color: #FFFFFF; 98 | background-color: #C82829 99 | } 100 | 101 | .code .invalid.deprecated { 102 | color: #FFFFFF; 103 | background-color: #8959A8 104 | } 105 | 106 | .code .fold { 107 | background-color: #4271AE; 108 | border-color: #4D4D4C 109 | } 110 | 111 | .code .entity.name.function, 112 | .code .support.function, 113 | .code .variable.parameter, 114 | .code .variable { 115 | color: #4271AE 116 | } 117 | 118 | .code .support.class, 119 | .code .support.type { 120 | color: #C99E00 121 | } 122 | 123 | .code .heading, 124 | .code .markup.heading, 125 | .code .string { 126 | color: #718C00 127 | } 128 | 129 | .code .entity.name.tag, 130 | .code .entity.other.attribute-name, 131 | .code .meta.tag, 132 | .code .string.regexp, 133 | .code .variable { 134 | color: #C82829 135 | } 136 | 137 | .code .comment { 138 | color: #8E908C 139 | } 140 | 141 | .code .indent-guide { 142 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==) right repeat-y 143 | } -------------------------------------------------------------------------------- /docs/graphqlapi/assets/require-by.css: -------------------------------------------------------------------------------- 1 | div.require-by.anyone, 2 | ul.require-by a { 3 | max-width: 100%; 4 | overflow: hidden; 5 | text-overflow: ellipsis; 6 | white-space: nowrap; 7 | display: block; 8 | } 9 | 10 | div.require-by.anyone { 11 | background-color: #f0f8fc; 12 | border: 1px solid #d8dde6; 13 | color: grey; 14 | padding: 2rem; 15 | text-align: center; 16 | margin: 1rem 0; 17 | border-radius: 0.25rem; 18 | } 19 | 20 | ul.require-by { 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | ul.require-by a { 26 | border-left: .25rem solid transparent; 27 | border-top: 1px solid transparent; 28 | border-bottom: 1px solid transparent; 29 | padding: .5rem 1.5rem; 30 | } 31 | 32 | ul.require-by a:hover { 33 | text-decoration: none; 34 | background-color: #f0f8fc; 35 | border-color: #d8dde6; 36 | border-left-color: #005fb2; 37 | } 38 | 39 | ul.require-by a em { 40 | margin-left: 1rem; 41 | font-size: .75rem; 42 | color: grey; 43 | } -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Bold.eot -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Bold.woff -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Bold.woff2 -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-BoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-BoldItalic.eot -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-BoldItalic.woff -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-BoldItalic.woff2 -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Italic.eot -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Italic.woff -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Italic.woff2 -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Light.eot -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Light.woff -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Light.woff2 -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-LightItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-LightItalic.eot -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-LightItalic.woff -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-LightItalic.woff2 -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Regular.eot -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Regular.woff -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Regular.woff2 -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Thin.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Thin.eot -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Thin.woff -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-Thin.woff2 -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-ThinItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-ThinItalic.eot -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-ThinItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-ThinItalic.woff -------------------------------------------------------------------------------- /docs/graphqlapi/fonts/webfonts/SalesforceSans-ThinItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/docs/graphqlapi/fonts/webfonts/SalesforceSans-ThinItalic.woff2 -------------------------------------------------------------------------------- /docs/graphqlapi/scripts/filter-types.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var HIDE_CLASS = 'slds-hide'; 3 | var ITEM_CLASS = 'slds-item'; 4 | 5 | /** 6 | * @class Item 7 | * @param {HTMLLIElement} li 8 | */ 9 | function Item(li) { 10 | this.li = li; 11 | this.type = li.title; 12 | this.typeLowerCase = li.title.toLowerCase(); 13 | } 14 | 15 | /** 16 | * @return boolean 17 | */ 18 | Item.prototype.contains = function (searchText) { 19 | return this.typeLowerCase.indexOf(searchText) >= 0; 20 | } 21 | 22 | /** 23 | * @return boolean 24 | */ 25 | Item.prototype.isHide = function () { 26 | this.li.classList.contains(HIDE_CLASS); 27 | } 28 | 29 | /** 30 | * @return void 31 | */ 32 | Item.prototype.hide = function () { 33 | if (!this.isHide()) 34 | this.li.classList.add(HIDE_CLASS); 35 | } 36 | 37 | /** 38 | * @return void 39 | */ 40 | Item.prototype.show = function () { 41 | this.li.classList.remove(HIDE_CLASS); 42 | } 43 | 44 | /** 45 | * @class ItemList 46 | * @param {Item[]} items 47 | */ 48 | function ItemList(items) { 49 | this.items = items; 50 | } 51 | 52 | /** 53 | * @function ItemsList.fromSelector 54 | * @param {string} selector 55 | * @return ItemList 56 | */ 57 | ItemList.fromSelector = function (selector) { 58 | 59 | var lis = document.querySelectorAll(selector); 60 | var items = Array.prototype.map.call(lis, function (li) { 61 | return new Item(li); 62 | }) 63 | 64 | return new ItemList(items); 65 | } 66 | 67 | /** 68 | * @return void 69 | */ 70 | ItemList.prototype.showIfmatch = function (match) { 71 | 72 | match = match.toLowerCase(match); 73 | 74 | this 75 | .items 76 | .forEach(function (item) { 77 | item.contains(match) ? 78 | item.show(): 79 | item.hide(); 80 | }) 81 | } 82 | 83 | /** 84 | * @var {ItemList} items 85 | * @var {HTMLInputElement} input 86 | */ 87 | var items = ItemList.fromSelector('nav .slds-navigation-list--vertical li'); 88 | var input = document.getElementById('type-search'); 89 | var lastMatch = ''; 90 | 91 | function onChange() { 92 | if (input.value === lastMatch) 93 | return; 94 | 95 | lastMatch = input.value; 96 | items.showIfmatch(lastMatch); 97 | } 98 | 99 | input.addEventListener('change', onChange); 100 | input.addEventListener('keyup', onChange); 101 | input.addEventListener('mouseup', onChange); 102 | })() -------------------------------------------------------------------------------- /docs/graphqlapi/scripts/focus-active.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var navScroll = document.getElementById('navication-scroll'); 3 | var header = document.querySelector('nav header'); 4 | var active = document.querySelector('.slds-is-active a'); 5 | 6 | if(active) 7 | navScroll.scrollTop = active.offsetTop - header.offsetHeight - Math.ceil(active.offsetHeight / 2) 8 | })() -------------------------------------------------------------------------------- /docs/graphqlapi/scripts/toggle-navigation.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var ACTIVE_CLASS = 'is-active'; 4 | var navigation = document.querySelector('nav'); 5 | var toggles = document.querySelectorAll('.js-toggle-navigation'); 6 | 7 | function toggleNavigation() { 8 | navigation.classList.contains(ACTIVE_CLASS) ? 9 | navigation.classList.remove(ACTIVE_CLASS) : 10 | navigation.classList.add(ACTIVE_CLASS); 11 | } 12 | 13 | Array.prototype.forEach.call( 14 | toggles, 15 | /** 16 | * @param {HTMLElement} toggle 17 | */ 18 | function (toggle) { 19 | toggle.addEventListener('click', toggleNavigation); 20 | } 21 | ) 22 | 23 | })() -------------------------------------------------------------------------------- /docs/graphqlapi/styles/_override.scss: -------------------------------------------------------------------------------- 1 | .slds-scrollable::-webkit-scrollbar { 2 | width: 10px; 3 | height: 10px 4 | } 5 | 6 | .slds-scrollable::-webkit-scrollbar:window-inactive { 7 | opacity: 0 8 | } 9 | 10 | .slds-scrollable::-webkit-scrollbar-thumb { 11 | background: #e0e5ee; 12 | border-radius: .5rem; 13 | box-shadow: #a8b7c7 0 0 0 1px inset 14 | } 15 | 16 | .slds-scrollable::-webkit-scrollbar-track { 17 | background: #a8b7c7 18 | } 19 | 20 | .slds-scrollable--y { 21 | -webkit-overflow-scrolling: touch; 22 | max-height: 100%; 23 | overflow: hidden; 24 | overflow-y: auto 25 | } 26 | 27 | .slds-scrollable--y::-webkit-scrollbar { 28 | width: 10px; 29 | height: 10px 30 | } 31 | 32 | .slds-scrollable--y::-webkit-scrollbar:window-inactive { 33 | opacity: 0 34 | } 35 | 36 | .slds-scrollable--y::-webkit-scrollbar-thumb { 37 | background: #e0e5ee; 38 | border-radius: .5rem; 39 | box-shadow: #a8b7c7 0 0 0 1px inset 40 | } 41 | 42 | .slds-scrollable--y::-webkit-scrollbar-track { 43 | background: #a8b7c7 44 | } 45 | 46 | .slds-scrollable--x { 47 | -webkit-overflow-scrolling: touch; 48 | max-width: 100%; 49 | overflow: hidden; 50 | overflow-x: auto 51 | } 52 | 53 | .slds-scrollable--x::-webkit-scrollbar { 54 | width: 10px; 55 | height: 10px 56 | } 57 | 58 | .slds-scrollable--x::-webkit-scrollbar:window-inactive { 59 | opacity: 0 60 | } 61 | 62 | .slds-scrollable--x::-webkit-scrollbar-thumb { 63 | background: #e0e5ee; 64 | border-radius: .5rem; 65 | box-shadow: #a8b7c7 0 0 0 1px inset 66 | } 67 | 68 | .slds-scrollable--x::-webkit-scrollbar-track { 69 | background: #a8b7c7 70 | } 71 | 72 | .slds-scrollable--x::-webkit-scrollbar-track, 73 | .slds-scrollable--y::-webkit-scrollbar-track { 74 | background: rgba(255, 255, 255, 0.1); 75 | } 76 | 77 | .slds-scrollable--x::-webkit-scrollbar-thumb, 78 | .slds-scrollable--y::-webkit-scrollbar-thumb { 79 | background: rgba(0, 0, 0, 0.1); 80 | box-shadow: none; 81 | } 82 | 83 | .material-icons.slds-button__icon { 84 | font-size: .875rem; 85 | vertical-align: text-bottom; 86 | } -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | // allow paren-less arrow functions 21 | 'arrow-parens': 0, 22 | // allow async-await 23 | 'generator-star-spacing': 0, 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | test/unit/coverage 8 | 9 | # Editor directories and files 10 | .idea 11 | .vscode 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | -------------------------------------------------------------------------------- /frontend/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | 20 | # run unit tests 21 | npm run unit 22 | 23 | # run all tests 24 | npm test 25 | ``` 26 | 27 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 28 | -------------------------------------------------------------------------------- /frontend/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, function (err, stats) { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | // if (stats.hasErrors()) { 31 | // console.log(chalk.red(' Build failed with errors.\n')) 32 | // process.exit(1) 33 | // } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /frontend/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | function exec (cmd) { 7 | return require('child_process').execSync(cmd).toString().trim() 8 | } 9 | 10 | const versionRequirements = [ 11 | { 12 | name: 'node', 13 | currentVersion: semver.clean(process.version), 14 | versionRequirement: packageConfig.engines.node 15 | } 16 | ] 17 | 18 | if (shell.which('npm')) { 19 | versionRequirements.push({ 20 | name: 'npm', 21 | currentVersion: exec('npm --version'), 22 | versionRequirement: packageConfig.engines.npm 23 | }) 24 | } 25 | 26 | module.exports = function () { 27 | const warnings = [] 28 | for (let i = 0; i < versionRequirements.length; i++) { 29 | const mod = versionRequirements[i] 30 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 31 | warnings.push(mod.name + ': ' + 32 | chalk.red(mod.currentVersion) + ' should be ' + 33 | chalk.green(mod.versionRequirement) 34 | ) 35 | } 36 | } 37 | 38 | if (warnings.length) { 39 | console.log('') 40 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 41 | console.log() 42 | for (let i = 0; i < warnings.length; i++) { 43 | const warning = warnings[i] 44 | console.log(' ' + warning) 45 | } 46 | console.log() 47 | process.exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict' 3 | require('eventsource-polyfill') 4 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 5 | 6 | hotClient.subscribe(function (event) { 7 | if (event.action === 'reload') { 8 | window.location.reload() 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /frontend/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | exports.assetsPath = function (_path) { 7 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 8 | ? config.build.assetsSubDirectory 9 | : config.dev.assetsSubDirectory 10 | return path.posix.join(assetsSubDirectory, _path) 11 | } 12 | 13 | exports.cssLoaders = function (options) { 14 | options = options || {} 15 | 16 | const cssLoader = { 17 | loader: 'css-loader', 18 | options: { 19 | minimize: process.env.NODE_ENV === 'production', 20 | sourceMap: options.sourceMap 21 | } 22 | } 23 | 24 | // generate loader string to be used with extract text plugin 25 | function generateLoaders (loader, loaderOptions) { 26 | const loaders = [cssLoader] 27 | if (loader) { 28 | loaders.push({ 29 | loader: loader + '-loader', 30 | options: Object.assign({}, loaderOptions, { 31 | sourceMap: options.sourceMap 32 | }) 33 | }) 34 | } 35 | 36 | // Extract CSS when that option is specified 37 | // (which is the case during production build) 38 | if (options.extract) { 39 | return ExtractTextPlugin.extract({ 40 | use: loaders, 41 | fallback: 'vue-style-loader' 42 | }) 43 | } else { 44 | return ['vue-style-loader'].concat(loaders) 45 | } 46 | } 47 | 48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 49 | return { 50 | css: generateLoaders(), 51 | postcss: generateLoaders(), 52 | less: generateLoaders('less'), 53 | sass: generateLoaders('sass', { indentedSyntax: true }), 54 | scss: generateLoaders('sass'), 55 | stylus: generateLoaders('stylus'), 56 | styl: generateLoaders('stylus') 57 | } 58 | } 59 | 60 | // Generate loaders for standalone style files (outside of .vue) 61 | exports.styleLoaders = function (options) { 62 | const output = [] 63 | const loaders = exports.cssLoaders(options) 64 | for (const extension in loaders) { 65 | const loader = loaders[extension] 66 | output.push({ 67 | test: new RegExp('\\.' + extension + '$'), 68 | use: loader 69 | }) 70 | } 71 | return output 72 | } 73 | -------------------------------------------------------------------------------- /frontend/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | 6 | module.exports = { 7 | loaders: utils.cssLoaders({ 8 | sourceMap: isProduction 9 | ? config.build.productionSourceMap 10 | : config.dev.cssSourceMap, 11 | extract: isProduction 12 | }), 13 | transformToRequire: { 14 | video: 'src', 15 | source: 'src', 16 | img: 'src', 17 | image: 'xlink:href' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | module.exports = { 12 | entry: { 13 | app: './src/main.js' 14 | }, 15 | output: { 16 | path: config.build.assetsRoot, 17 | filename: '[name].js', 18 | publicPath: process.env.NODE_ENV === 'production' 19 | ? config.build.assetsPublicPath 20 | : config.dev.assetsPublicPath 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.vue', '.json'], 24 | alias: { 25 | 'vue$': 'vue/dist/vue.esm.js', 26 | '@': resolve('src'), 27 | } 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.(js|vue)$/, 33 | loader: 'eslint-loader', 34 | enforce: 'pre', 35 | include: [resolve('src'), resolve('test')], 36 | options: { 37 | formatter: require('eslint-friendly-formatter') 38 | } 39 | }, 40 | { 41 | test: /\.vue$/, 42 | loader: 'vue-loader', 43 | options: vueLoaderConfig 44 | }, 45 | { 46 | test: /\.js$/, 47 | loader: 'babel-loader', 48 | include: [resolve('src'), resolve('test')] 49 | }, 50 | { 51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 52 | loader: 'url-loader', 53 | options: { 54 | limit: 10000, 55 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 56 | } 57 | }, 58 | { 59 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 60 | loader: 'url-loader', 61 | options: { 62 | limit: 10000, 63 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 64 | } 65 | }, 66 | { 67 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 68 | loader: 'url-loader', 69 | options: { 70 | limit: 10000, 71 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 72 | } 73 | } 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 9 | 10 | // add hot-reload related code to entry chunks 11 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 12 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 13 | }) 14 | 15 | module.exports = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 18 | }, 19 | // cheap-module-eval-source-map is faster for development 20 | devtool: '#cheap-module-eval-source-map', 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': config.dev.env 24 | }), 25 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoEmitOnErrorsPlugin(), 28 | // https://github.com/ampedandwired/html-webpack-plugin 29 | new HtmlWebpackPlugin({ 30 | filename: 'index.html', 31 | template: 'index.html', 32 | inject: true 33 | }), 34 | new FriendlyErrorsPlugin() 35 | ] 36 | }) 37 | -------------------------------------------------------------------------------- /frontend/build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // This is the webpack config used for unit tests. 3 | 4 | const utils = require('./utils') 5 | const webpack = require('webpack') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | 9 | const webpackConfig = merge(baseWebpackConfig, { 10 | // use inline sourcemap for karma-sourcemap-loader 11 | module: { 12 | rules: utils.styleLoaders() 13 | }, 14 | devtool: '#inline-source-map', 15 | resolveLoader: { 16 | alias: { 17 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 18 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 19 | 'scss-loader': 'sass-loader' 20 | } 21 | }, 22 | plugins: [ 23 | new webpack.DefinePlugin({ 24 | 'process.env': require('../config/test.env') 25 | }) 26 | ] 27 | }) 28 | 29 | // no need for app entry during tests 30 | delete webpackConfig.entry 31 | 32 | module.exports = webpackConfig 33 | -------------------------------------------------------------------------------- /frontend/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/config/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | // Template version: 1.1.3 4 | // see http://vuejs-templates.github.io/webpack for documentation. 5 | 6 | const path = require('path') 7 | 8 | module.exports = { 9 | build: { 10 | env: require('./prod.env'), 11 | index: path.resolve(__dirname, '../dist/index.html'), 12 | assetsRoot: path.resolve(__dirname, '../dist'), 13 | assetsSubDirectory: 'static', 14 | assetsPublicPath: '/', 15 | productionSourceMap: true, 16 | // Gzip off by default as many popular static hosts such as 17 | // Surge or Netlify already gzip all static assets for you. 18 | // Before setting to `true`, make sure to: 19 | // npm install --save-dev compression-webpack-plugin 20 | productionGzip: false, 21 | productionGzipExtensions: ['js', 'css'], 22 | // Run the build command with an extra argument to 23 | // View the bundle analyzer report after build finishes: 24 | // `npm run build --report` 25 | // Set to `true` or `false` to always turn it on or off 26 | bundleAnalyzerReport: process.env.npm_config_report 27 | }, 28 | dev: { 29 | env: require('./dev.env'), 30 | port: process.env.PORT || 8080, 31 | autoOpenBrowser: true, 32 | assetsSubDirectory: 'static', 33 | assetsPublicPath: '/', 34 | proxyTable: {}, 35 | // CSS Sourcemaps off by default because relative paths are "buggy" 36 | // with this option, according to the CSS-Loader README 37 | // (https://github.com/webpack/css-loader#sourcemaps) 38 | // In our experience, they generally work as expected, 39 | // just be aware of this issue when enabling this option. 40 | cssSourceMap: false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /frontend/config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | frontend 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/axioshelpers.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const queryAllDeals = ` 4 | query allDeals { 5 | deals { 6 | edges{ 7 | node{ 8 | uid, 9 | name, 10 | amount, 11 | dealState, 12 | contact { 13 | firstname 14 | lastname 15 | } 16 | } 17 | } 18 | } 19 | } 20 | ` 21 | 22 | export function getAllDeals() { 23 | return axios.post(`http://27c9db6b.ngrok.io/api`, {'query': queryAllDeals }, {'headers':{'Content-Type':'application/json'}}) 24 | } -------------------------------------------------------------------------------- /frontend/src/components/Deal.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /frontend/src/components/DealsList.vue: -------------------------------------------------------------------------------- 1 | 21 | 62 | 63 | 64 | 82 | -------------------------------------------------------------------------------- /frontend/src/components/DealsPending.vue: -------------------------------------------------------------------------------- 1 | 18 | 51 | 52 | 53 | 71 | -------------------------------------------------------------------------------- /frontend/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 12 | 22 | 23 | 24 | 43 | -------------------------------------------------------------------------------- /frontend/src/components/Reports.vue: -------------------------------------------------------------------------------- 1 | 12 | 20 | 21 | 22 | 41 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import axios from 'axios'; 7 | Vue.config.productionTip = false 8 | 9 | 10 | /* eslint-disable no-new */ 11 | const crmapp = new Vue({ 12 | el: '#app', 13 | router, 14 | template: '', 15 | components: { App } 16 | }) 17 | console.log(crmapp) 18 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Reports from '@/components/Reports' 4 | import Home from '@/components/Home' 5 | import DealsList from '@/components/DealsList' 6 | import PendingDeals from '@/components/DealsPending' 7 | 8 | 9 | Vue.use(Router) 10 | 11 | export default new Router({ 12 | routes: [ 13 | { 14 | path: '/', 15 | name: 'Home', 16 | component: Home 17 | }, 18 | { 19 | path:'/deals', 20 | name: 'DealsList', 21 | component: DealsList 22 | }, 23 | { 24 | path:'/reports', 25 | name: 'Reports', 26 | component: Reports 27 | }, 28 | { path:'/pendingdeals', 29 | name: 'PendingDeals', 30 | component: PendingDeals 31 | } 32 | 33 | ] 34 | }) 35 | -------------------------------------------------------------------------------- /frontend/test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /frontend/test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' } 30 | ] 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /frontend/test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HelloWorld from '@/components/HelloWorld' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .to.equal('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage(){ 4 | echo "Usage:" 5 | echo -e "\t./prepare --prod (Install Production dependencies)" 6 | echo -e "\t./prepare --dev (Install Development dependencies)" 7 | echo "" 8 | exit 1 9 | } 10 | 11 | environment="" 12 | 13 | 14 | if [ "$1" != "" ]; then 15 | if [ "$1" == "--prod" ];then 16 | environment="prod" 17 | elif [ "$1" == "--dev" ];then 18 | environment="dev" 19 | else 20 | usage 21 | fi 22 | 23 | else 24 | usage 25 | fi 26 | 27 | 28 | 29 | echo '************************************' 30 | echo 'Installing System-Level Dependencies' 31 | echo '************************************' 32 | echo '' 33 | 34 | if [ $(uname) == 'Linux' ];then 35 | cat requirements.apt | xargs sudo apt-get install -y 36 | sudo curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - 37 | else 38 | xargs brew install < requirements.brew 39 | fi 40 | 41 | clear 42 | 43 | echo '************************************' 44 | echo 'Installing Python-Level Dependencies' 45 | echo '************************************' 46 | echo '' 47 | 48 | if [ "$environment" == "prod" ]; then 49 | pip3 install -r requirements.pip 50 | else 51 | pip3 install -r requirements-testing.pip 52 | fi 53 | 54 | clear 55 | 56 | echo '************************************' 57 | echo 'Installing Nodejs-Level Dependencies' 58 | echo '************************************' 59 | echo '' 60 | 61 | if [ "$environment" == "prod" ]; then 62 | echo -e 'No production level npm packages required\n' 63 | else 64 | cat requirements.npm | sudo xargs npm install -g 65 | fi 66 | 67 | echo 'DONE!' 68 | -------------------------------------------------------------------------------- /requirements-dev.apt: -------------------------------------------------------------------------------- 1 | python3-pip 2 | python3-dev 3 | libffi-dev 4 | nodejs 5 | redis-server 6 | postgresql 7 | postgresql-contrib 8 | curl 9 | -------------------------------------------------------------------------------- /requirements-testing.pip: -------------------------------------------------------------------------------- 1 | alembic==0.9.5 2 | certifi==2017.7.27.1 3 | cffi==1.11.0 4 | chardet==3.0.4 5 | click==6.7 6 | coverage==4.4.2 7 | decorator==4.1.2 8 | ecdsa==0.13 9 | Faker==0.7.18 10 | Flask==0.12.2 11 | Flask-Admin==1.5.0 12 | Flask-Cache==0.13.1 13 | Flask-GraphQL==1.4.1 14 | Flask-Migrate==2.1.0 15 | Flask-Misaka==0.4.1 16 | Flask-Script==2.0.5 17 | flask-shell-ipython==0.3.0 18 | Flask-SQLAlchemy==2.2 19 | future==0.16.0 20 | graphene==2.0.dev20170802065539 21 | graphene-sqlalchemy==2.0.dev2017083101 22 | graphql-core==2.0.dev20170801051721 23 | graphql-relay==0.4.5 24 | idna==2.6 25 | inbox.py==0.0.6 26 | infinity==1.4 27 | intervals==0.8.0 28 | ipython==6.2.1 29 | ipython-genutils==0.2.0 30 | iso8601==0.1.12 31 | itsdangerous==0.24 32 | jedi==0.11.0 33 | Jinja2==2.9.6 34 | Logbook==1.1.0 35 | Mako==1.0.7 36 | MarkupSafe==1.0 37 | misaka==2.1.0 38 | nose==1.3.7 39 | parso==0.1.0 40 | pexpect==4.2.1 41 | pickleshare==0.7.4 42 | pkg-resources==0.0.0 43 | promise==2.1 44 | prompt-toolkit==1.0.15 45 | psycopg2==2.7.3.1 46 | ptyprocess==0.5.2 47 | pyblake2==0.9.3 48 | pycparser==2.18 49 | pycrypto==2.6.1 50 | Pygments==2.2.0 51 | python-dateutil==2.6.1 52 | python-editor==1.0.3 53 | python-http-client==3.0.0 54 | python-jose==1.4.0 55 | redis==2.10.6 56 | requests==2.18.4 57 | rq==0.9.2 58 | sendgrid==5.3.0 59 | simplegeneric==0.8.1 60 | singledispatch==3.4.0.3 61 | six==1.11.0 62 | SQLAlchemy==1.1.13 63 | SQLAlchemy-Utils==0.32.16 64 | traitlets==4.3.2 65 | typing==3.6.2 66 | ujson==1.35 67 | urllib3==1.22 68 | uWSGI==2.0.15 69 | validators==0.12.0 70 | wcwidth==0.1.7 71 | Werkzeug==0.12.2 72 | WTForms==2.1 73 | WTForms-Alchemy==0.16.5 74 | WTForms-Components==0.10.3 75 | -------------------------------------------------------------------------------- /requirements.apt: -------------------------------------------------------------------------------- 1 | python3-dev 2 | libffi-dev 3 | -------------------------------------------------------------------------------- /requirements.brew: -------------------------------------------------------------------------------- 1 | libffi 2 | python3 3 | node -------------------------------------------------------------------------------- /requirements.npm: -------------------------------------------------------------------------------- 1 | @2fd/graphdoc 2 | -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- 1 | alembic==0.9.5 2 | certifi==2017.7.27.1 3 | cffi==1.11.0 4 | chardet==3.0.4 5 | click==6.7 6 | decorator==4.1.2 7 | ecdsa==0.13 8 | Faker==0.7.18 9 | Flask==0.12.2 10 | Flask-Admin==1.5.0 11 | Flask-Cache==0.13.1 12 | Flask-GraphQL==1.4.1 13 | Flask-Migrate==2.1.0 14 | Flask-Misaka==0.4.1 15 | Flask-Script==2.0.5 16 | flask-shell-ipython==0.3.0 17 | Flask-SQLAlchemy==2.2 18 | future==0.16.0 19 | graphene==2.0.dev20170802065539 20 | graphene-sqlalchemy==2.0.dev2017083101 21 | graphql-core==2.0.dev20170801051721 22 | graphql-relay==0.4.5 23 | idna==2.6 24 | inbox.py==0.0.6 25 | infinity==1.4 26 | intervals==0.8.0 27 | ipython==6.2.1 28 | ipython-genutils==0.2.0 29 | iso8601==0.1.12 30 | itsdangerous==0.24 31 | jedi==0.11.0 32 | Jinja2==2.9.6 33 | Logbook==1.1.0 34 | Mako==1.0.7 35 | MarkupSafe==1.0 36 | misaka==2.1.0 37 | parso==0.1.0 38 | pexpect==4.2.1 39 | pickleshare==0.7.4 40 | promise==2.1 41 | prompt-toolkit==1.0.15 42 | psycopg2==2.7.3.1 43 | ptyprocess==0.5.2 44 | pyblake2==0.9.3 45 | pycparser==2.18 46 | pycrypto==2.6.1 47 | Pygments==2.2.0 48 | python-dateutil==2.6.1 49 | python-editor==1.0.3 50 | python-http-client==3.0.0 51 | python-jose==1.4.0 52 | redis==2.10.6 53 | requests==2.18.4 54 | rq==0.9.2 55 | sendgrid==5.3.0 56 | simplegeneric==0.8.1 57 | singledispatch==3.4.0.3 58 | six==1.11.0 59 | SQLAlchemy==1.1.13 60 | SQLAlchemy-Utils==0.32.16 61 | traitlets==4.3.2 62 | typing==3.6.2 63 | ujson==1.35 64 | urllib3==1.22 65 | uWSGI==2.0.15 66 | validators==0.12.0 67 | wcwidth==0.1.7 68 | Werkzeug==0.12.2 69 | WTForms==2.1 70 | WTForms-Alchemy==0.16.5 71 | WTForms-Components==0.10.3 72 | inbox.py==0.0.6 73 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | nosetests --with-coverage --cover-package=crm --cover-html --cover-erase -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='crm', 5 | packages=['crm'], 6 | include_package_data=True, 7 | install_requires=[ 8 | 'flask', 9 | 'uwsgi', 10 | ], 11 | ) 12 | -------------------------------------------------------------------------------- /specs/AlertManagement.md: -------------------------------------------------------------------------------- 1 | 2 | ## principles 3 | 4 | - part of CRM app 5 | 6 | ## model 7 | 8 | source 9 | - uid 10 | - title 11 | - description (markdown) 12 | - comments 13 | - itenvironment (link to itenvironment obj if it exists, see assetmgmt) 14 | - project (link to project object) 15 | - links (list of link objects) 16 | 17 | 18 | alert 19 | - uid 20 | - title 21 | - source (link to source) 22 | - source_id #whatever id used on source to identify where the issue comes from (recommend dot notation) 23 | - content (markdown) 24 | - category (dot notation, freely to be chosen by source) 25 | - parentalert (if this is dependent on other alert) 26 | - deviceuid (is assetmgmt used) 27 | - componentuid (is assetmgmt used) 28 | - starttime (auto calculated by system) 29 | - closetime (auto calculated by system) 30 | - state: new, confirmed, closed 31 | - urgency: critical, urgent, normal, minor 32 | - data (for e.g. toml, json or yaml structured content) 33 | - escalationlevel (yellow, orrange, red, green) 34 | - alert_profile 35 | - comments 36 | - tasks 37 | - links 38 | - owner 39 | #can be more than 1 at same time 40 | - type_down: bool (if there is downtime) 41 | - type_performance: bool 42 | - type_security: book 43 | - type_instability: bool 44 | - type_ui :userinterface 45 | - type_possibledataloss 46 | 47 | alert_profile 48 | - uid 49 | - name 50 | - description (markdown) 51 | - configuration: is toml config, depends on type of profile, what this configuration is 52 | -------------------------------------------------------------------------------- /specs/Beta2Test.md: -------------------------------------------------------------------------------- 1 | 2 | ## what 3 | 4 | describe some use cases for testing our beta2 5 | 6 | ## test 1 7 | 8 | - add task to user/contact/deal/project/sprint/company 9 | - be able to assign 1 user to a task (from user table) 10 | - make sure this task is visible in details view 11 | - make sure we can query the task from task filter list 12 | 13 | ## test 2 14 | 15 | - make sure user can see his assigned tasks & filter on user/contact/deal/project/sprint/company (linked to task) 16 | - this needs to be easy for a user 17 | 18 | ## test 3 19 | 20 | - everyone logs in over IYO, each modified object needs to show author (original & modified) so we know who changed & when 21 | - this needs to be visible in each object in detail mode 22 | 23 | ## test 4 24 | 25 | - test messages, comments & links to user/contact/deal/project/sprint/company 26 | - in detail view on comments we need to see author & moddate (so in relation to last change) 27 | 28 | ## test 5 29 | 30 | - export all data 31 | - in each json there needs to be subobj: tasks, messages, links, comments 32 | - remove the DB 33 | - import all data back 34 | - crm needs to have all data 35 | 36 | ## test 6 37 | 38 | - create python script which on server works with the flask objects & does some manipulations against postgresql 39 | 40 | ## test 10 41 | 42 | - create a python test program, is linked to a subgroup which has rights to crm 43 | - from this test program do a graphql query 44 | -------------------------------------------------------------------------------- /specs/MailinMailout.md: -------------------------------------------------------------------------------- 1 | ## mail in 2 | 3 | - use python mailserver which processes all incoming mails & attaches them to appropriate object 4 | - e.g. https://github.com/kennethreitz/inbox.py 5 | - mail addresses used will be 6 | - $uid_$objtype@$maildomain e.g. 4jd3_contact@main.threefoldtoken.com 7 | - support@$maildomain will be attached to project: support as ticket 8 | - only recognized senders will be allowed to send 9 | - the author field is used to show where mail comes from (who is originator) 10 | - if author not recognized then we send message back to see we don't recognize you in our system please send email to support@$maildomain to get help 11 | - attachments will be stored in $webdir/files/$blakehash.$extension & linked into the message (markdown) 12 | 13 | ## mail out 14 | 15 | - depending the object we decide to who to send emails e.g. project will send to all users & contacts & companies attached to project, ... 16 | - if the destination field was used before sending then use the email addr as stated there 17 | - we need a state field to know what the state of the email is: 18 | - tosend, failed, ... 19 | 20 | 21 | -------------------------------------------------------------------------------- /specs/Redis.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | - when delete/change/new for a data object 4 | - put in redis 5 | - hset: $nameofobj(table):$uid -> value: the toml which is the dataobj 6 | - queue: 7 | - msgpack list: 8 | - user_name 9 | - user_email 10 | - nameofobj e.g. task 11 | - uid 12 | - epoch 13 | - so we know what to process from hset 14 | - a separate python process will 15 | - write data to git structure & commit with username 16 | -------------------------------------------------------------------------------- /specs/Requirements.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | - use as much as possible the tools (lots of autogeneration possible) 4 | - keep everything as simple as possible 5 | - the UID is always the 4 char generated code (lower case number/letter e.g. 5bhs), check dublicates, generate otherone if duplicate 6 | - hosted behind caddy (can do the IYO integration & pass the info) 7 | -------------------------------------------------------------------------------- /specs/beta2_basic_test_usecases.md: -------------------------------------------------------------------------------- 1 | 2 | ## what 3 | 4 | describe some use cases for testing our beta2 5 | 6 | ## test 1 7 | 8 | - add task to user/contact/deal/project/sprint/company 9 | - be able to assign 1 user to a task (from user table) 10 | - make sure this task is visible in details view 11 | - make sure we can query the task from task filter list 12 | 13 | ## test 2 14 | 15 | - make sure user can see his assigned tasks & filter on user/contact/deal/project/sprint/company (linked to task) 16 | - this needs to be easy for a user 17 | 18 | ## test 3 19 | 20 | - everyone logs in over IYO, each modified object needs to show author (original & modified) so we know who changed & when 21 | - this needs to be visible in each object in detail mode 22 | 23 | ## test 4 24 | 25 | - test messages, comments & links to user/contact/deal/project/sprint/company 26 | - in detail view on comments we need to see author & moddate (so in relation to last change) 27 | 28 | ## test 5 29 | 30 | - export all data 31 | - in each json there needs to be subobj: tasks, messages, links, comments 32 | - remove the DB 33 | - import all data back 34 | - crm needs to have all data 35 | 36 | ## test 6 37 | 38 | - create python script which on server works with the flask objects & does some manipulations against postgresql 39 | 40 | ## test 10 41 | 42 | - create a python test program, is linked to a subgroup which has rights to crm 43 | - from this test program do a graphql query 44 | 45 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Incubaid/crm/5f68ebb3236e1dfeb8378e11591b865a89004bac/tests/__init__.py -------------------------------------------------------------------------------- /tests/base_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import tempfile 4 | import sys 5 | 6 | # crm_base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | # sys.path.insert(0, crm_base) 8 | import crm 9 | 10 | 11 | 12 | 13 | class BaseTestCase(unittest.TestCase): 14 | """ 15 | Base testcase 16 | """ 17 | 18 | def setUp(self): 19 | """ 20 | Setup 21 | """ 22 | self.db_fd, crm.app.config['DATABASE'] = tempfile.mkstemp() 23 | crm.app.testing = True 24 | self.app = crm.app.test_client() 25 | 26 | 27 | def tearDown(self): 28 | """ 29 | Teardown 30 | """ 31 | os.close(self.db_fd) 32 | os.unlink(crm.app.config['DATABASE']) 33 | 34 | 35 | if __name__ == '__main__': 36 | unittest.main() -------------------------------------------------------------------------------- /tests/test_home.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for home module 3 | """ 4 | import unittest 5 | from tests.base_tests import BaseTestCase 6 | 7 | class HomeTest(BaseTestCase): 8 | """ 9 | Test for home module 10 | """ 11 | def test_home(self): 12 | """ 13 | Test home page 14 | """ 15 | rv = self.app.get('/') 16 | assert rv.status_code == 200 17 | assert b'Quick links' in rv.data 18 | 19 | 20 | if __name__ == '__main__': 21 | unittest.main() -------------------------------------------------------------------------------- /utils/google_spreadsheet_example.gs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | How to apply: 4 | 5 | Edit -> Current project's trigger then (setFilter, on change) 6 | Resource -> Advanced google services -> 7 | 8 | */ 9 | 10 | function setFilter() { 11 | 12 | var ss = SpreadsheetApp.getActiveSpreadsheet(); 13 | var import = ss.getSheetByName('IMPORT') 14 | var list = ss.getSheetByName('List') 15 | 16 | list.clear() 17 | 18 | // Copy All data 19 | var data = import.getRange('A:Z').getValues() 20 | for (var i=0; i < data.length; i++){ 21 | list.appendRow(data[i]) 22 | } 23 | 24 | // Apply filter to list 25 | 26 | var request = { 27 | "setBasicFilter": { 28 | "filter": { 29 | "range" : { 30 | sheetId: list.getSheetId() 31 | }, 32 | 33 | "criteria": { 34 | 4 : { 'hiddenValues': ['AMBASSADOR', 'ITFT']}, 35 | 5 : {'hiddenValues': ['NEW', 'CLOSED']} 36 | } 37 | } 38 | } 39 | }; 40 | 41 | // Apply filter 42 | var data = Sheets.Spreadsheets.batchUpdate({'requests': [request]}, ss.getId()); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http = 0.0.0.0:5000 3 | module = app:app 4 | processes = 4 5 | threads = 2 6 | stats = 127.0.0.1:9191 7 | die-on-term = true --------------------------------------------------------------------------------