├── .circleci
└── config.yml
├── .dockerignore
├── .gitignore
├── .gitmodules
├── Dockerfile
├── LICENSE.txt
├── MANIFEST.in
├── README.md
├── adaero
├── __init__.py
├── alembic.ini
├── config.py
├── constants.py
├── data_migrations
│ └── __init__.py
├── database.py
├── date.py
├── example.ini
├── forms.py
├── gunicorn
│ ├── config.py
│ └── wsgi.py
├── history.py
├── host_example.ini
├── mail.py
├── migration.py
├── migrations
│ ├── README
│ ├── env.py
│ ├── script.py.mako
│ └── versions
│ │ └── aa9125020ba8_initial_commit.py
├── models
│ ├── __init__.py
│ ├── all.py
│ ├── period.py
│ └── user.py
├── population.py
├── scripts
│ ├── __init__.py
│ ├── configure_db.py
│ └── feedback_app.py
├── security
│ ├── __init__.py
│ └── ldapauth.py
├── static
│ └── assets
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ └── logo.png
├── stats.py
├── templates
│ ├── __init__.py
│ └── email
│ │ ├── __init__.py
│ │ ├── approve.html.j2
│ │ ├── base.html.j2
│ │ ├── enrol.html.j2
│ │ ├── entry.html.j2
│ │ ├── invite.html.j2
│ │ ├── review.html.j2
│ │ └── tm_confirmation.html.j2
├── text.py
└── views
│ ├── __init__.py
│ ├── auth.py
│ ├── error.py
│ ├── external.py
│ ├── feedback.py
│ ├── manager.py
│ ├── metadata.py
│ ├── nomination.py
│ └── talent_manager.py
├── architecture.dot
├── architecture.png
├── docker
├── dev
│ └── docker-compose.yml
├── prod
│ ├── docker-compose.yml
│ └── nginx
│ │ ├── adaero.conf
│ │ └── nginx.conf
└── shared
│ └── ldif
│ └── 01-data.ldif
├── docs
├── login.png
├── user-guide.pdf
└── welcome.png
├── frontend
├── .editorconfig
├── .gitignore
├── Dockerfile
├── README.md
├── angular.json
├── browserslist
├── e2e
│ ├── app.e2e-spec.ts
│ ├── app.po.ts
│ └── tsconfig.e2e.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor.conf.js
├── proxy.conf.json
├── proxy.local.conf.json
├── src
│ ├── app
│ │ ├── app-routing.module.ts
│ │ ├── app.component.html
│ │ ├── app.component.scss
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ │ ├── components
│ │ │ ├── views
│ │ │ │ ├── authenticated
│ │ │ │ │ ├── authenticated.component.html
│ │ │ │ │ ├── authenticated.component.scss
│ │ │ │ │ ├── authenticated.component.spec.ts
│ │ │ │ │ └── authenticated.component.ts
│ │ │ │ ├── external-invite
│ │ │ │ │ ├── external-invite.component.html
│ │ │ │ │ ├── external-invite.component.scss
│ │ │ │ │ ├── external-invite.component.spec.ts
│ │ │ │ │ └── external-invite.component.ts
│ │ │ │ ├── feedback-about-me
│ │ │ │ │ ├── feedback-about-me.component.html
│ │ │ │ │ ├── feedback-about-me.component.scss
│ │ │ │ │ ├── feedback-about-me.component.spec.ts
│ │ │ │ │ └── feedback-about-me.component.ts
│ │ │ │ ├── feedback-history-view
│ │ │ │ │ ├── feedback-history-view.component.html
│ │ │ │ │ ├── feedback-history-view.component.scss
│ │ │ │ │ ├── feedback-history-view.component.spec.ts
│ │ │ │ │ └── feedback-history-view.component.ts
│ │ │ │ ├── give-feedback
│ │ │ │ │ ├── give-feedback.component.html
│ │ │ │ │ ├── give-feedback.component.scss
│ │ │ │ │ └── give-feedback.component.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── login
│ │ │ │ │ ├── login.component.html
│ │ │ │ │ ├── login.component.scss
│ │ │ │ │ ├── login.component.spec.ts
│ │ │ │ │ └── login.component.ts
│ │ │ │ ├── nominees-list
│ │ │ │ │ ├── nominees-list.component.html
│ │ │ │ │ ├── nominees-list.component.scss
│ │ │ │ │ └── nominees-list.component.ts
│ │ │ │ ├── self-nominate
│ │ │ │ │ ├── self-nominate.component.html
│ │ │ │ │ ├── self-nominate.component.spec.ts
│ │ │ │ │ └── self-nominate.component.ts
│ │ │ │ ├── stats
│ │ │ │ │ ├── stats.component.html
│ │ │ │ │ ├── stats.component.scss
│ │ │ │ │ ├── stats.component.spec.ts
│ │ │ │ │ └── stats.component.ts
│ │ │ │ ├── summarise-feedback
│ │ │ │ │ ├── summarise-feedback.component.html
│ │ │ │ │ ├── summarise-feedback.component.scss
│ │ │ │ │ ├── summarise-feedback.component.spec.ts
│ │ │ │ │ └── summarise-feedback.component.ts
│ │ │ │ └── talent-manager-panel
│ │ │ │ │ ├── talent-manager-panel.component.html
│ │ │ │ │ ├── talent-manager-panel.component.scss
│ │ │ │ │ ├── talent-manager-panel.component.spec.ts
│ │ │ │ │ └── talent-manager-panel.component.ts
│ │ │ └── widgets
│ │ │ │ ├── feedback-form
│ │ │ │ ├── feedback-form.component.html
│ │ │ │ ├── feedback-form.component.scss
│ │ │ │ ├── feedback-form.component.spec.ts
│ │ │ │ └── feedback-form.component.ts
│ │ │ │ ├── feedback-history
│ │ │ │ ├── feedback-history.component.html
│ │ │ │ ├── feedback-history.component.scss
│ │ │ │ ├── feedback-history.component.spec.ts
│ │ │ │ └── feedback-history.component.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── modal
│ │ │ │ ├── modal.component.html
│ │ │ │ ├── modal.component.scss
│ │ │ │ ├── modal.component.spec.ts
│ │ │ │ └── modal.component.ts
│ │ ├── guards
│ │ │ ├── anon-guard.service.spec.ts
│ │ │ ├── anon-guard.service.ts
│ │ │ ├── auth-guard.service.spec.ts
│ │ │ ├── auth-guard.service.ts
│ │ │ ├── index.ts
│ │ │ ├── pending-changes-guard.service.spec.ts
│ │ │ └── pending-changes-guard.service.ts
│ │ ├── pipes
│ │ │ ├── nominee-filter.pipe.spec.ts
│ │ │ └── nominee-filter.pipe.ts
│ │ └── services
│ │ │ ├── api.service.ts
│ │ │ ├── cookie.service.spec.ts
│ │ │ └── cookie.service.ts
│ ├── assets
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ └── logo.png
│ ├── environments
│ │ ├── environment.prod.ts
│ │ └── environment.ts
│ ├── index.html
│ ├── main.ts
│ ├── polyfills.ts
│ ├── styles.scss
│ ├── test.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.spec.json
│ └── typings.d.ts
├── tsconfig.json
└── tslint.json
├── gunicorn_starter.sh
├── install-packages.sh
├── population.dot
├── population.png
├── setup.cfg
├── setup.py
└── tests
├── __init__.py
├── integration
├── README.md
├── __init__.py
├── conftest.py
├── constants.py
├── models
│ ├── __init__.py
│ ├── test_feedback.py
│ └── test_period.py
└── views
│ ├── __init__.py
│ ├── company_stats.csv
│ ├── conftest.py
│ ├── test_auth.py
│ ├── test_external.py
│ ├── test_feedback.py
│ ├── test_mail.py
│ ├── test_manager.py
│ ├── test_metadata.py
│ ├── test_nomination.py
│ └── test_talent_manager.py
├── scripts
├── __init__.py
└── configure_db.py
├── settings.py
└── unit
├── README.md
├── __init__.py
├── population.csv
├── test_config.py
├── test_date.py
├── test_period.py
├── test_population.py
├── test_security.py
└── test_text.py
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2 # use CircleCI 2.0
2 | jobs: # A basic unit of work in a run
3 | build: # runs not using Workflows must have a `build` job as entry point
4 | # directory where steps are run
5 | docker: # run the steps with Docker
6 | # CircleCI Python images available at: https://hub.docker.com/r/circleci/python/
7 | - image: circleci/python:3.6.4
8 | steps:
9 | - checkout
10 | - run: sudo apt-get update
11 | - run: sudo apt-get install libldap2-dev
12 | - run: sudo apt-get install libsasl2-dev
13 | - run: sudo python setup.py develop
14 | - run: sudo pip install .[test]
15 | - run: py.test tests/
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 |
3 | /*.egg
4 | /*.log
5 | /*egg-info
6 | /.cache
7 | /.coverage
8 | /.coverage.*
9 | /.coveragerc
10 | /.project
11 | /.pydevproject
12 | /.settings
13 | */__pycache__
14 | /build
15 | /coverage.xml
16 | /dist
17 | docs/autodoc
18 | /junit.xml
19 | /pylint.xml
20 | /temp
21 | /tmp
22 | /.eggs
23 | tests/integration/.coverage*
24 | tests/integration/__pycache__
25 | tests/unit/__pycache__
26 | *.pyc
27 |
28 | /*.idea
29 | *.DS_Store
30 | *.sqlite3
31 | # generated static assets should never be stored in repo
32 | adaero/static/3rdpartylicenses.txt
33 | adaero/static/*.js
34 | adaero/static/*.map
35 | adaero/static/*.bundle.css
36 | adaero/static/index.html
37 |
38 | docker
39 | tests
40 | frontend
41 | venv
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /*.egg
2 | /*.log
3 | /*egg-info
4 | /.cache
5 | /.coverage
6 | /.coverage.*
7 | /.coveragerc
8 | /.project
9 | /.pydevproject
10 | /.settings
11 | */__pycache__
12 | /build
13 | /coverage.xml
14 | /dist
15 | docs/autodoc
16 | /junit.xml
17 | /pylint.xml
18 | /temp
19 | /tmp
20 | /.eggs
21 | tests/integration/.coverage*
22 | tests/integration/__pycache__
23 | tests/unit/__pycache__
24 | *.pyc
25 |
26 | /*.idea
27 | *.DS_Store
28 | *.sqlite3
29 | # generated static assets should never be stored in repo
30 | adaero/static/3rdpartylicenses.txt
31 | adaero/static/*.js
32 | adaero/static/*.map
33 | adaero/static/*.css
34 | adaero/static/index.html
35 | adaero/local.ini
36 | docker/bin/docker_common.sh
37 | adaero/test.ini
38 | frontend/package-lock.json
39 |
40 | .tox
41 | venv
42 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/.gitmodules
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:18.04
2 |
3 | WORKDIR /usr/src/app
4 | COPY install-packages.sh .
5 | RUN ./install-packages.sh
6 | RUN curl https://bootstrap.pypa.io/get-pip.py | python3.6
7 | COPY setup.py .
8 | RUN pip3.6 install -e .
9 | COPY adaero adaero
10 | COPY gunicorn_starter.sh .
11 |
12 | EXPOSE 8080
13 | ENTRYPOINT ["./gunicorn_starter.sh"]
14 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt *.ini *.cfg *.rst
2 | recursive-include feedback *.png *.css *.gif *.jpg *.txt *.js *.html *.xml
3 |
--------------------------------------------------------------------------------
/adaero/__init__.py:
--------------------------------------------------------------------------------
1 | # $HeadURL$
2 | """ This is the main python package for adaero.
3 | """
4 |
5 | from pyramid.config import Configurator
6 |
7 |
8 | def main(global_config, **settings): # pylint: disable=unused-argument
9 | """ This function returns a Pyramid WSGI application.
10 | """
11 | config = Configurator(settings=settings)
12 | config.include(".security")
13 | config.include(".models")
14 | config.include(".mail")
15 | config.include("pyramid_beaker")
16 | config.include("rest_toolkit")
17 | # migrations do not form part of web app runtime
18 | config.scan(ignore="adaero.migrations")
19 | # views include must happen after scan or else scan will not work because
20 | # include overrides
21 | config.include(".views")
22 | return config.make_wsgi_app()
23 |
--------------------------------------------------------------------------------
/adaero/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = migrations
6 |
7 | # template used to generate migration files
8 | # file_template = %%(rev)s_%%(slug)s
9 |
10 | # max length of characters to apply to the
11 | # "slug" field
12 | #truncate_slug_length = 40
13 |
14 | # set to 'true' to run the environment during
15 | # the 'revision' command, regardless of autogenerate
16 | # revision_environment = false
17 |
18 | # set to 'true' to allow .pyc and .pyo files without
19 | # a source .py file to be detected as revisions in the
20 | # versions/ directory
21 | # sourceless = false
22 |
23 | # version location specification; this defaults
24 | # to adaero/migrations/versions. When using multiple version
25 | # directories, initial revisions must be specified with --version-path
26 | # version_locations = %(here)s/bar %(here)s/bat adaero/migrations/versions
27 |
28 | # the output encoding used when revision files
29 | # are written from script.py.mako
30 | # output_encoding = utf-8
31 |
32 | # should passed in programmatically
33 | # sqlalchemy.url = driver://user:pass@localhost/dbname
34 | sqlalchemy.url = postgres://postgres:mysecretpassword@localhost:5432
35 |
36 |
37 | # Logging configuration
38 | [loggers]
39 | keys = root,sqlalchemy,alembic
40 |
41 | [handlers]
42 | keys = console
43 |
44 | [formatters]
45 | keys = generic
46 |
47 | [logger_root]
48 | level = WARN
49 | handlers = console
50 | qualname =
51 |
52 | [logger_sqlalchemy]
53 | level = WARN
54 | handlers =
55 | qualname = sqlalchemy.engine
56 |
57 | [logger_alembic]
58 | level = INFO
59 | handlers =
60 | qualname = alembic
61 |
62 | [handler_console]
63 | class = StreamHandler
64 | args = (sys.stderr,)
65 | level = NOTSET
66 | formatter = generic
67 |
68 | [formatter_generic]
69 | format = %(levelname)-5.5s [%(name)s] %(message)s
70 | datefmt = %H:%M:%S
71 |
--------------------------------------------------------------------------------
/adaero/config.py:
--------------------------------------------------------------------------------
1 | import getpass
2 | import os
3 | import socket
4 |
5 | from logging import getLogger as get_logger
6 | from adaero import constants
7 |
8 | log = get_logger(__name__)
9 |
10 |
11 | def get_envvar_name(key):
12 | return key.split(".")[-1].upper()
13 |
14 |
15 | def get_config_value(settings, key, default=None, raise_if_not_set=False):
16 | """
17 | For a given `key`, provide a value to resolves first to an environment
18 | variable, else the Pyramid config, else lastly, a default if provided.
19 |
20 | Parameters
21 | ----------
22 | settings
23 | key
24 | default:
25 | raise_if_not_set:
26 | If `True`, then default is ignored if unset and throw an exception.
27 |
28 | Returns
29 | -------
30 | Configuration value to use
31 | """
32 | envvar_key = get_envvar_name(key)
33 | val = os.getenv(envvar_key, settings.get(key))
34 | if isinstance(val, str):
35 | if val.lower() == "false":
36 | val = False
37 | elif val.lower() == "true":
38 | val = True
39 | if val is not None:
40 | return val
41 | else:
42 | if raise_if_not_set:
43 | raise ValueError(
44 | "`{}` is not set! Please set and try " "again.".format(key)
45 | )
46 | return default
47 |
48 |
49 | def check_if_production(settings):
50 | hostname = socket.gethostname()
51 | unix_user = getpass.getuser()
52 | configured_hostname = get_config_value(settings, constants.PRODUCTION_HOSTNAME_KEY)
53 | configured_user = get_config_value(settings, constants.PRODUCTION_USER_KEY)
54 | is_production = hostname == configured_hostname and unix_user == configured_user
55 | if is_production:
56 | log.warning(
57 | "Configured production hostname and user match current "
58 | "environment so running in production mode."
59 | )
60 | else:
61 | log.warning(
62 | "Configured production hostname and user (%s, %s) don't "
63 | "match the current environment (%s, %s) so not running in "
64 | "production mode."
65 | % (configured_hostname, configured_user, hostname, unix_user)
66 | )
67 | return is_production
68 |
--------------------------------------------------------------------------------
/adaero/constants.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | CONNECTION_STRING = "oracle+cx_oracle://%s:%s@%s"
5 |
6 | # all *_KEY below have a corresponding environment variable. please look at
7 | # `adaero/config.py` for more details.
8 | DB_URL_KEY = "adaero.db_url"
9 | DB_HOST_KEY = "adaero.db_host"
10 | DB_PORT_KEY = "adaero.db_port"
11 | DB_NAME_KEY = "adaero.db_name"
12 | DB_SERVICE_NAME_KEY = "adaero.db_service_name"
13 | BUSINESS_UNIT_KEY = "adaero.business_unit"
14 | HOMEBASE_LOCATION_KEY = "adaero.homebase_location"
15 | TALENT_MANAGER_USERNAMES_KEY = "adaero.talent_manager_usernames"
16 | RELOAD_USERS_ON_APP_START_KEY = "adaero.reload_users_on_app_start"
17 | LOAD_TALENT_MANAGERS_ON_APP_START_KEY = (
18 | "adaero.load_talent_managers_" "on_app_start"
19 | )
20 | CORS_ALLOW_ORIGIN_KEY = "adaero.cors.allow_origin"
21 | ALLOW_PASSWORDLESS_ACCESS_KEY = "adaero.allow_passwordless_access"
22 | LOAD_USER_EMAIL_LIST_KEY = "adaero.load_user_email_list"
23 | CHECK_AND_SEND_EMAIL_INT_KEY = "adaero.check_and_send_email_interval_s"
24 | RUN_EMAIL_INTERVAL_JOB_KEY = "adaero.run_email_interval_job"
25 | TALENT_MANAGER_ON_EMAIL_KEY = "adaero.talent_manager_on_email"
26 | SERVED_ON_HTTPS_KEY = "adaero.served_on_https"
27 | FRONTEND_SERVER_PORT_KEY = "adaero.frontend_server_port"
28 | PRODUCTION_HOSTNAME_KEY = "adaero.production_hostname"
29 | PRODUCTION_USER_KEY = "adaero.production_user"
30 | EMAIL_START_DELAY_S_KEY = "adaero.email_start_delay_s"
31 | EMAIL_DELAY_BETWEEN_S_KEY = "adaero.email_delay_between_s"
32 | ENABLE_SEND_EMAIL_KEY = "adaero.enable_send_email"
33 | DISPLAYED_HOSTNAME_KEY = "adaero.displayed_hostname"
34 | DATABASE_REVISION_KEY = "adaero.database_revision"
35 | LDAP_URI_KEY = "adaero.ldap_uri"
36 | LDAP_USER_BIND_TEMPLATE_KEY = "adaero.ldap_user_bind_template"
37 | LDAP_USERNAME_KEY = "adaero.ldap_username_key"
38 | LDAP_MANAGER_KEY = "adaero.ldap_manager_key"
39 | LDAP_LOCATION_KEY = "adaero.ldap_location_key"
40 | LDAP_UID_KEY = "adaero.ldap_uid_key"
41 | LDAP_DEPARTMENT_KEY = "adaero.ldap_department_key"
42 | LDAP_BUSINESS_UNIT_KEY = "adaero.ldap_business_unit_key"
43 | LDAP_SEARCH_BIND_DN_KEY = "adaero.ldap_search_bind_dn"
44 | LDAP_SEARCH_PASSWORD_KEY = "adaero.ldap_search_password"
45 | LDAP_BASE_DN_KEY = "adaero.ldap_base_dn"
46 | LDAP_DN_USERNAME_ATTRIBUTE_KEY = "adaero.ldap_dn_username_attribute"
47 | LDAP_DN_USERNAME_REGEX_KEY = "adaero.ldap_dn_username_regex"
48 |
49 | COMPANY_NAME_KEY = "adaero.company_name"
50 | SUPPORT_EMAIL_KEY = "adaero.support_email"
51 | REPLY_EMAIL_KEY = "adaero.reply_email"
52 | LOGIN_USERNAME_MSG_KEY = "adaero.login_username_msg"
53 | LOGIN_PASSWORD_MSG_KEY = "adaero.login_password_msg"
54 | TM_UPLOAD_NEW_POPULATION_MSG_KEY = "adaero.tm_upload_new_population_msg"
55 | TM_GENERATE_POPULATION_MSG_KEY = "adaero.tm_generate_population_msg"
56 | LOGO_FILENAME_KEY = "adaero.logo_filename"
57 |
58 | DEFAULT_DISPLAY_DATETIME_FORMAT = "%H:%M%p, %d %B %Y"
59 | ENROL_START = "enrol_start"
60 | ENTRY_START = "entry_start"
61 | APPROVE_START = "approve_start"
62 | REVIEW_START = "review_start"
63 | ENROL_REMINDER = "enrol_reminder"
64 | ENTRY_REMINDER = "entry_reminder"
65 | APPROVE_REMINDER = "approve_reminder"
66 | EMAIL_TEMPLATE_MAP = {
67 | ENROL_START: {
68 | "summary": "Opportunity to Enrol",
69 | "template": "enrol.html.j2",
70 | "audience": "employee",
71 | "code": "ust01",
72 | },
73 | ENROL_REMINDER: {
74 | "summary": "Opportunity to Enrol Reminder",
75 | "template": "enrol.html.j2",
76 | "audience": "non-nominated",
77 | "code": "ust02",
78 | },
79 | ENTRY_START: {
80 | "summary": "Provide Feedback",
81 | "template": "entry.html.j2",
82 | "audience": "employee",
83 | "code": "ust03",
84 | },
85 | ENTRY_REMINDER: {
86 | "summary": "Provide Feedback Reminder",
87 | "template": "entry.html.j2",
88 | "audience": "employee",
89 | "code": "ust04",
90 | },
91 | APPROVE_START: {
92 | "summary": "Manager Review and Schedule Feedback Discussions",
93 | "template": "approve.html.j2",
94 | "audience": "manager",
95 | "code": "ust05",
96 | },
97 | REVIEW_START: {
98 | "summary": "Your feedback is available",
99 | "template": "review.html.j2",
100 | "audience": "summarised",
101 | "code": "ust06",
102 | },
103 | APPROVE_REMINDER: {
104 | "summary": "Manager Review and Schedule Feedback Discussions Reminder",
105 | "template": "approve.html.j2",
106 | "audience": "manager",
107 | "code": "ust07",
108 | },
109 | }
110 | AUDIENCE_VALUES = {t["audience"] for t in EMAIL_TEMPLATE_MAP.values()}
111 | EMAIL_CODES = {"ust01", "ust02", "ust03", "ust04", "ust05", "ust06", "ust07"}
112 | ANSWER_CHAR_LIMIT = 60000
113 |
114 |
115 | # If the below number is changed, please update:
116 | # * `frontend/components/views/feedback-about-me.component`
117 | # * `frontend/components/views/feedback-history-view.component`
118 | MANAGER_VIEW_HISTORY_LIMIT = 3
119 | MISCONFIGURATION_MESSAGE = "{error}. Please refer to the readme on how to set."
120 |
--------------------------------------------------------------------------------
/adaero/data_migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/adaero/data_migrations/__init__.py
--------------------------------------------------------------------------------
/adaero/database.py:
--------------------------------------------------------------------------------
1 | from logging import getLogger as get_logger
2 |
3 | from sqlalchemy import create_engine
4 | from sqlalchemy.pool import QueuePool
5 |
6 | from adaero import config, constants
7 |
8 |
9 | log = get_logger(__name__)
10 |
11 |
12 | def prepare_db(settings):
13 | db_url = config.get_config_value(
14 | settings, constants.DB_URL_KEY, raise_if_not_set=True
15 | )
16 | log.info("Connecting to DB %s", db_url)
17 | return create_engine(
18 | db_url,
19 | pool_size=5,
20 | max_overflow=40,
21 | echo_pool=True,
22 | pool_recycle=300,
23 | poolclass=QueuePool,
24 | echo=False,
25 | )
26 |
--------------------------------------------------------------------------------
/adaero/date.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from datetime import timedelta
5 |
6 | from pytz import timezone
7 |
8 | # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
9 |
10 | LONDON = "London"
11 | NEW_YORK = "New York"
12 | OXFORD = "Oxford"
13 | SHANGHAI = "Shanghai"
14 | HONG_KONG = "Hong Kong"
15 | GBR = "GBR"
16 | BOSTON = "Boston"
17 | PFAFFIKON = "Pfäffikon"
18 | USA = "USA"
19 | CUSTOM_LOC_TO_PYTZ_LOC = {
20 | LONDON: "Europe/London",
21 | NEW_YORK: "America/New_York",
22 | OXFORD: "Europe/London",
23 | SHANGHAI: "Asia/Shanghai",
24 | HONG_KONG: "Asia/Hong_Kong",
25 | GBR: "Europe/London",
26 | BOSTON: "US/Eastern",
27 | PFAFFIKON: "Europe/Zurich",
28 | USA: "US/Eastern",
29 | }
30 |
31 |
32 | def datetimeformat(
33 | utc_naive_dt, user, format_="%-d %B 18:00 (%Z)", fallback_location=LONDON
34 | ):
35 | """
36 | By default, 1 April 18:00 (HKT)
37 | * To stay on the safe side, displayed datetime on email is
38 | 1 day prior, but no change to actual phase timing.
39 | * This means HK will have move time to complete
40 | * US has less time
41 | * Will not work for anytime zones UTC-6
42 | """
43 | if format_.count("18:00"):
44 | utc_naive_dt -= timedelta(days=1)
45 | elif 0 < utc_naive_dt.minute < 30:
46 | utc_naive_dt -= timedelta(minutes=utc_naive_dt.minute)
47 | elif 30 < utc_naive_dt.minute <= 59:
48 | utc_naive_dt -= timedelta(minutes=utc_naive_dt.minute - 30)
49 | if user.location and user.location in list(CUSTOM_LOC_TO_PYTZ_LOC.keys()):
50 | valid_loc = user.location
51 | else:
52 | valid_loc = fallback_location
53 | tz = timezone(CUSTOM_LOC_TO_PYTZ_LOC[valid_loc])
54 | return tz.localize(utc_naive_dt).strftime(format_)
55 |
56 |
57 | def adjust_dt_for_location(dt, location, fallback_location=LONDON):
58 | if location and location in list(CUSTOM_LOC_TO_PYTZ_LOC.keys()):
59 | valid_loc = location
60 | else:
61 | valid_loc = fallback_location
62 | return (
63 | timezone(CUSTOM_LOC_TO_PYTZ_LOC[valid_loc])
64 | .localize(dt)
65 | .astimezone(timezone("UTC"))
66 | .replace(tzinfo=None)
67 | )
68 |
--------------------------------------------------------------------------------
/adaero/example.ini:
--------------------------------------------------------------------------------
1 | ###
2 | # app configuration
3 | # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
4 | ###
5 |
6 | [app:adaero]
7 | use = egg:adaero
8 | pyramid.debug_authorization = false
9 | pyramid.debug_notfound = false
10 | pyramid.debug_routematch = false
11 | pyramid.default_locale_name = en
12 |
13 | adaero.debug_all = true
14 | adaero.db_url = postgres://postgres:mysecretpassword@db:5432
15 |
16 | # sessions
17 | beaker.session.type = memory
18 | beaker.session.secret = ibetterbeareallylongrandomkeyinproduction
19 | beaker.session.httponly = true
20 |
21 | # Please refer to the README.md for more information
22 | adaero.business_unit =
23 | adaero.homebase_location = London
24 | adaero.enable_send_email = false
25 | adaero.served_on_https = false
26 | adaero.talent_manager_usernames = ["eforshaw"]
27 | adaero.reload_users_on_app_start = false
28 | adaero.allow_passwordless_access = true
29 | adaero.run_email_interval_job = false
30 | adaero.check_and_send_email_interval_s = 600
31 | adaero.frontend_server_port = 4200
32 | adaero.email_start_delay_s = 5
33 | adaero.email_delay_between_s = 0.5
34 |
35 | adaero.ldap_uri = ldap://ldap:389
36 | adaero.ldap_username_key = uid
37 | adaero.ldap_manager_key = manager
38 | adaero.ldap_user_bind_template = uid={username},ou=People,dc=example,dc=org
39 | adaero.ldap_location_key = l
40 | adaero.ldap_uid_key = uidNumber
41 | adaero.ldap_department_key = departmentNumber
42 | adaero.ldap_business_unit_key = o
43 | adaero.ldap_search_bind_dn = cn=admin,dc=example,dc=org
44 | adaero.ldap_search_password = admin
45 | adaero.ldap_base_dn = ou=People,dc=example,dc=org
46 | adaero.ldap_dn_username_attribute = uid
47 | adaero.ldap_dn_username_regex = uid=(\w*)
48 |
49 | adaero.company_name = Example Org.
50 | adaero.support_email = support@example.com
51 | adaero.reply_email = no-reply@example.com
52 | adaero.login_username_msg = Enter your LDAP username
53 | adaero.login_password_msg = Enter your LDAP password
54 | adaero.tm_upload_new_population_msg = Please upload a CSV that has the same columns as the downloaded or generated CSV files. Use the PHPLDAPAdmin tool to find out more information about users.
55 | adaero.tm_generate_population_msg = Enter business unit i.e. Engineering
56 |
57 |
58 |
59 | rest_toolkit.debug = True
60 |
61 | ###
62 | # wsgi server configuration
63 | ###
64 |
65 | [server:main]
66 | use = egg:gunicorn#main
67 | # not using localhost to ensure we don't serve on ::0
68 | host = 0.0.0.0
69 | port = 8080
70 |
71 | [filter:translogger]
72 | use = egg:Paste#translogger
73 | setup_console_handler = False
74 |
75 |
76 | ###
77 | # logging configuration
78 | # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
79 | # to hide wsgi logs, remove `translogger` from the pipeline
80 | ###
81 |
82 | [pipeline:main]
83 | pipeline = translogger
84 | adaero
85 |
86 | [loggers]
87 | keys = root, adaero, sqlalchemy, txn
88 |
89 | [handlers]
90 | keys = console
91 |
92 | [formatters]
93 | keys = generic
94 |
95 | [logger_root]
96 | level = DEBUG
97 | handlers = console
98 |
99 | [logger_adaero]
100 | level = DEBUG
101 | handlers = console
102 | qualname = adaero
103 | propagate = 0
104 |
105 | [logger_sqlalchemy]
106 | level = WARN
107 | handlers = console
108 | qualname = sqlalchemy.engine
109 | propagate = 0
110 | # "level = INFO" logs SQL queries.
111 | # "level = DEBUG" logs SQL queries and results.
112 | # "level = WARN" logs neither. (Recommended for production systems.)
113 |
114 | [logger_txn]
115 | level = WARN
116 | handlers = console
117 | qualname = txn
118 |
119 | [handler_console]
120 | class = StreamHandler
121 | args = (sys.stderr,)
122 | level = NOTSET
123 | formatter = generic
124 |
125 | [formatter_generic]
126 | format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s] %(message)s
127 |
--------------------------------------------------------------------------------
/adaero/gunicorn/config.py:
--------------------------------------------------------------------------------
1 | logconfig = "/opt/app-root/src/adaero/example.ini"
2 |
--------------------------------------------------------------------------------
/adaero/gunicorn/wsgi.py:
--------------------------------------------------------------------------------
1 | from paste.deploy import loadapp
2 |
3 | application = loadapp("config:/opt/app-root/src/adaero/example.ini")
4 |
--------------------------------------------------------------------------------
/adaero/history.py:
--------------------------------------------------------------------------------
1 | import transaction
2 | from sqlalchemy import and_, desc
3 |
4 | from adaero.config import get_config_value
5 | from adaero.models import Period, FeedbackForm, Nominee, User
6 | from adaero import constants
7 |
8 |
9 | def fetch_feedback_history(dbsession, username, settings, fetch_full=False):
10 | """
11 | Parameters
12 | ----------
13 | dbsession: `sqlalchemy.session.Session`
14 | username: `str`
15 | Username of user to fetch the feedback for
16 | settings: `dict`
17 | Global settings that Pyramid generates from the ini file
18 | fetch_full: `bool`
19 | If `False`, only fetch the latest
20 | `constants.MANAGER_VIEW_HISTORY_LIMIT` feedbacks
21 |
22 | Returns
23 | -------
24 | JSON-serialisable payload that contains the feedback history of
25 | provided user.
26 | """
27 | location = get_config_value(settings, constants.HOMEBASE_LOCATION_KEY)
28 | q = dbsession.query(Period, FeedbackForm, Nominee)
29 | with transaction.manager:
30 | user = dbsession.query(User).get(username)
31 | history = (
32 | q.outerjoin(
33 | FeedbackForm,
34 | and_(
35 | FeedbackForm.period_id == Period.id,
36 | FeedbackForm.to_username == username,
37 | FeedbackForm.is_summary == True,
38 | ),
39 | ) # noqa
40 | .outerjoin(
41 | Nominee,
42 | and_(Period.id == Nominee.period_id, Nominee.username == username),
43 | )
44 | .order_by(desc(Period.enrollment_start_utc))
45 | )
46 |
47 | if fetch_full:
48 | history = history.all()
49 | else:
50 | history = history.limit(constants.MANAGER_VIEW_HISTORY_LIMIT)
51 |
52 | feedbacks = []
53 | for period, summary_form, nominee in history:
54 | if period.subperiod(location) != Period.REVIEW_SUBPERIOD:
55 | feedbacks.append(
56 | {
57 | "periodDescription": "%s pending" % period.name,
58 | "enable": False,
59 | "items": [],
60 | }
61 | )
62 | elif not summary_form and not nominee:
63 | feedbacks.append(
64 | {
65 | "periodDescription": "Did not request feedback for period "
66 | "%s" % period.name,
67 | "enable": False,
68 | "items": [],
69 | }
70 | )
71 | elif not summary_form:
72 | feedbacks.append(
73 | {
74 | "periodDescription": "No feedback available for period %s"
75 | % period.name,
76 | "enable": False,
77 | "items": [],
78 | }
79 | )
80 | else:
81 | feedback = {
82 | "periodDescription": period.name,
83 | "enable": True,
84 | "items": [],
85 | }
86 | ordered_questions = sorted(
87 | [qu for qu in period.template.rows], key=lambda x: x.position
88 | )
89 | answers_by_q_id = {f.question_id: f for f in summary_form.answers}
90 | ordered_rows = [
91 | answers_by_q_id[r.question.id] for r in ordered_questions
92 | ]
93 | for answer in ordered_rows:
94 | item = {
95 | "question": answer.question.question_template.format(
96 | display_name=user.display_name, period_name=period.name
97 | ),
98 | "answer": answer.content,
99 | }
100 | feedback["items"].append(item)
101 | feedbacks.append(feedback)
102 |
103 | return {"feedback": {"displayName": user.display_name, "items": feedbacks}}
104 |
--------------------------------------------------------------------------------
/adaero/host_example.ini:
--------------------------------------------------------------------------------
1 | ###
2 | # app configuration
3 | # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
4 | ###
5 |
6 | [app:adaero]
7 | use = egg:adaero
8 | pyramid.debug_authorization = false
9 | pyramid.debug_notfound = false
10 | pyramid.debug_routematch = false
11 | pyramid.default_locale_name = en
12 |
13 | adaero.debug_all = true
14 | adaero.db_url = postgres://postgres:mysecretpassword@localhost:5432
15 |
16 | # sessions
17 | beaker.session.type = memory
18 | beaker.session.secret = ibetterbeareallylongrandomkeyinproduction
19 | beaker.session.httponly = true
20 |
21 | # Please refer to the README.md for more information
22 | adaero.homebase_location = London
23 | adaero.enable_send_email = false
24 | adaero.served_on_https = false
25 | adaero.talent_manager_usernames = ["eforshaw"]
26 | adaero.reload_users_on_app_start = false
27 | adaero.allow_passwordless_access = true
28 | adaero.run_email_interval_job = false
29 | adaero.check_and_send_email_interval_s = 600
30 | adaero.frontend_server_port = 4200
31 | adaero.email_start_delay_s = 5
32 | adaero.email_delay_between_s = 0.5
33 |
34 | adaero.ldap_uri = ldap://localhost:389
35 | adaero.ldap_username_key = uid
36 | adaero.ldap_manager_key = manager
37 | adaero.ldap_user_bind_template = uid={username},ou=People,dc=example,dc=org
38 | adaero.ldap_location_key = l
39 | adaero.ldap_uid_key = uidNumber
40 | adaero.ldap_department_key = departmentNumber
41 | adaero.ldap_business_unit_key = o
42 | adaero.ldap_search_bind_dn = cn=admin,dc=example,dc=org
43 | adaero.ldap_search_password = admin
44 | adaero.ldap_base_dn = ou=People,dc=example,dc=org
45 | adaero.ldap_dn_username_attribute = uid
46 | adaero.ldap_dn_username_regex = uid=(\w*)
47 |
48 | adaero.company_name = Example Org.
49 | adaero.support_email = support@example.com
50 | adaero.reply_email = no-reply@example.com
51 | adaero.login_username_msg = Enter your LDAP username
52 | adaero.login_password_msg = Enter your LDAP password
53 | adaero.tm_upload_new_population_msg = Please upload a CSV that has the same columns as the downloaded or generated CSV files. Use the PHPLDAPAdmin tool to find out more information about users.
54 | adaero.tm_generate_population_msg = Enter business unit i.e. Engineering
55 |
56 |
57 |
58 | rest_toolkit.debug = True
59 |
60 | ###
61 | # wsgi server configuration
62 | ###
63 |
64 | [server:main]
65 | use = egg:gunicorn#main
66 | # not using localhost to ensure we don't serve on ::0
67 | host = 0.0.0.0
68 | port = 8080
69 |
70 | [filter:translogger]
71 | use = egg:Paste#translogger
72 | setup_console_handler = False
73 |
74 |
75 | ###
76 | # logging configuration
77 | # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
78 | # to hide wsgi logs, remove `translogger` from the pipeline
79 | ###
80 |
81 | [pipeline:main]
82 | pipeline = translogger
83 | adaero
84 |
85 | [loggers]
86 | keys = root, adaero, sqlalchemy, txn
87 |
88 | [handlers]
89 | keys = console
90 |
91 | [formatters]
92 | keys = generic
93 |
94 | [logger_root]
95 | level = DEBUG
96 | handlers = console
97 |
98 | [logger_adaero]
99 | level = DEBUG
100 | handlers = console
101 | qualname = adaero
102 | propagate = 0
103 |
104 | [logger_sqlalchemy]
105 | level = WARN
106 | handlers = console
107 | qualname = sqlalchemy.engine
108 | propagate = 0
109 | # "level = INFO" logs SQL queries.
110 | # "level = DEBUG" logs SQL queries and results.
111 | # "level = WARN" logs neither. (Recommended for production systems.)
112 |
113 | [logger_txn]
114 | level = WARN
115 | handlers = console
116 | qualname = txn
117 |
118 | [handler_console]
119 | class = StreamHandler
120 | args = (sys.stderr,)
121 | level = NOTSET
122 | formatter = generic
123 |
124 | [formatter_generic]
125 | format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s] %(message)s
126 |
--------------------------------------------------------------------------------
/adaero/migration.py:
--------------------------------------------------------------------------------
1 | import alembic.config
2 | import os
3 | import pkg_resources
4 |
5 | from adaero import config, constants
6 |
7 |
8 | def generate_alembic_config(settings):
9 | pth = pkg_resources.resource_filename("adaero", "alembic.ini")
10 | os.chdir(os.path.dirname(pth))
11 | alembic_cfg = alembic.config.Config(pth)
12 | return alembic_cfg
13 |
14 |
15 | def upgrade_db_to_configured_revision(settings):
16 | alembic_cfg = generate_alembic_config(settings)
17 | revision = config.get_config_value(
18 | settings, constants.DATABASE_REVISION_KEY, default="head"
19 | )
20 | if not revision:
21 | raise ValueError(
22 | constants.MISCONFIGURATION_MESSAGE.format(
23 | error="Database revision is not set"
24 | )
25 | )
26 | alembic.command.upgrade(alembic_cfg, revision=revision)
27 |
--------------------------------------------------------------------------------
/adaero/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration. Please refer to the adaero
2 | README.md for more information.
--------------------------------------------------------------------------------
/adaero/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | from alembic import context
3 | from sqlalchemy import engine_from_config, pool
4 | from logging.config import fileConfig
5 |
6 | from adaero.models.all import Base
7 |
8 | # this is the Alembic Config object, which provides
9 | # access to the values within the .ini file in use.
10 | config = context.config
11 |
12 | # Interpret the config file for Python logging.
13 | # This line sets up loggers basically.
14 | fileConfig(config.config_file_name)
15 |
16 | # Configuration for autogeneration of schema migration scripts
17 | target_metadata = Base.metadata
18 |
19 | # other values from the config, defined by the needs of env.py,
20 | # can be acquired:
21 | # my_important_option = config.get_main_option("my_important_option")
22 | # ... etc.
23 |
24 |
25 | def run_migrations_offline():
26 | """Run migrations in 'offline' mode.
27 |
28 | This configures the context with just a URL
29 | and not an Engine, though an Engine is acceptable
30 | here as well. By skipping the Engine creation
31 | we don't even need a DBAPI to be available.
32 |
33 | Calls to context.execute() here emit the given string to the
34 | script output.
35 |
36 | """
37 | url = config.get_main_option("sqlalchemy.url")
38 | context.configure(url=url, target_metadata=target_metadata)
39 |
40 | with context.begin_transaction():
41 | context.run_migrations()
42 |
43 |
44 | def run_migrations_online():
45 | """Run migrations in 'online' mode.
46 |
47 | In this scenario we need to create an Engine
48 | and associate a connection with the context.
49 |
50 | """
51 | engine = engine_from_config(
52 | config.get_section(config.config_ini_section),
53 | prefix="sqlalchemy.",
54 | poolclass=pool.NullPool,
55 | )
56 |
57 | connection = engine.connect()
58 | context.configure(connection=connection, target_metadata=target_metadata)
59 |
60 | try:
61 | with context.begin_transaction():
62 | context.run_migrations()
63 | finally:
64 | connection.close()
65 |
66 |
67 | if context.is_offline_mode():
68 | run_migrations_offline()
69 | else:
70 | run_migrations_online()
71 |
--------------------------------------------------------------------------------
/adaero/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 |
9 | # revision identifiers, used by Alembic.
10 | revision = ${repr(up_revision)}
11 | down_revision = ${repr(down_revision)}
12 | branch_labels = ${repr(branch_labels)}
13 | depends_on = ${repr(depends_on)}
14 |
15 | from alembic import op
16 | import sqlalchemy as sa
17 | ${imports if imports else ""}
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/adaero/scripts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/adaero/scripts/__init__.py
--------------------------------------------------------------------------------
/adaero/scripts/feedback_app.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from pyramid.scripts import pserve
4 | import pkg_resources
5 |
6 |
7 | def main():
8 | config_ini_file = sys.argv[1]
9 | config_path = pkg_resources.resource_filename("adaero", config_ini_file)
10 | return pserve.main(argv=["pserve", config_path])
11 |
--------------------------------------------------------------------------------
/adaero/security/__init__.py:
--------------------------------------------------------------------------------
1 | from logging import getLogger as get_logger
2 | from pyramid.authentication import SessionAuthenticationPolicy
3 | from pyramid.authorization import ACLAuthorizationPolicy
4 | from pyramid.events import NewRequest
5 | from pyramid.security import Authenticated, Everyone
6 |
7 | from adaero import constants
8 | from adaero.config import get_config_value, get_envvar_name, check_if_production
9 | from adaero.models.user import request_user_callback
10 | from adaero.security.ldapauth import request_ldapauth_callback
11 |
12 | log = get_logger(__name__)
13 |
14 | USER_SESSION_KEY = "user"
15 | # cookie named aligned with frontend
16 | # https://angular.io/api/common/http/HttpClientXsrfModule
17 | ANGULAR_2_XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN"
18 | ANGULAR_2_XSRF_TOKEN_HEADER_NAME = "X-XSRF-TOKEN"
19 | PYRAMID_XSRF_TOKEN_COOKIE_NAME = "X-CSRF-Token"
20 |
21 | EXTERNAL_BUSINESS_UNIT_ROLE = "role:external_business_unit"
22 | EMPLOYEE_ROLE = "role:employee"
23 | MANAGER_ROLE = "role:manager"
24 | TALENT_MANAGER_ROLE = "role:talent_manager"
25 | DIRECT_REPORT_PREFIX = "direct_report:"
26 | TRADING_DEPARTMENT_TEMPLATE = "Trading - {}"
27 |
28 |
29 | def add_cors_callback_builder(allow_origin_domain):
30 | def add_cors(event):
31 |
32 | headers = "Origin, Content-Type, Accept, Authorization"
33 |
34 | def cors_headers(request, response):
35 | origin = allow_origin_domain
36 | response.headers.update(
37 | {
38 | # In production you would be careful with this
39 | "Access-Control-Allow-Origin": origin,
40 | "Access-Control-Allow-Headers": headers,
41 | "Access-Control-Allow-Credentials": "true",
42 | }
43 | )
44 |
45 | event.request.add_response_callback(cors_headers)
46 |
47 | return add_cors
48 |
49 |
50 | class SimpleAuthenticationPolicy(SessionAuthenticationPolicy):
51 | def authenticated_userid(self, request):
52 | user = request.user
53 | if user is not None:
54 | return user.username
55 | return None
56 |
57 | def effective_principals(self, request):
58 | principals = [Everyone]
59 | user = request.user
60 | if not user:
61 | return principals
62 |
63 | principals.append(Authenticated)
64 | principals.append(user.username)
65 |
66 | if user.is_staff:
67 | principals.append(EMPLOYEE_ROLE)
68 | else:
69 | principals.append(EXTERNAL_BUSINESS_UNIT_ROLE)
70 |
71 | if user.has_direct_reports:
72 | principals.append(MANAGER_ROLE)
73 | for direct_report_user in user.direct_reports:
74 | principal_string = DIRECT_REPORT_PREFIX + direct_report_user.username
75 | principals.append(principal_string)
76 |
77 | if (
78 | user.username
79 | in request.registry.settings[constants.TALENT_MANAGER_USERNAMES_KEY]
80 | ):
81 | principals.append(TALENT_MANAGER_ROLE)
82 |
83 | return principals
84 |
85 |
86 | def setup_cors(config):
87 | settings = config.get_settings()
88 | allow_origin_string = get_config_value(settings, constants.CORS_ALLOW_ORIGIN_KEY)
89 | if allow_origin_string:
90 | log.warning(
91 | "CORS enabled. Access-Control-Allow-Origin will be "
92 | "restricted to %s" % allow_origin_string
93 | )
94 | config.add_subscriber(
95 | add_cors_callback_builder(allow_origin_string), NewRequest
96 | )
97 |
98 |
99 | def includeme(config):
100 |
101 | settings = config.get_settings()
102 |
103 | if get_config_value(settings, constants.ALLOW_PASSWORDLESS_ACCESS_KEY):
104 | log.warning(
105 | "PASSWORDLESS ACCESS IS ENABLED (has been set in "
106 | "config %s or envvar %s)"
107 | % (
108 | constants.ALLOW_PASSWORDLESS_ACCESS_KEY,
109 | get_envvar_name(constants.ALLOW_PASSWORDLESS_ACCESS_KEY),
110 | )
111 | )
112 |
113 | authn_policy = SimpleAuthenticationPolicy(callback=None)
114 | config.set_authentication_policy(authn_policy)
115 | config.set_authorization_policy(ACLAuthorizationPolicy())
116 | config.set_default_csrf_options(
117 | require_csrf=True, header=ANGULAR_2_XSRF_TOKEN_HEADER_NAME
118 | )
119 | config.add_request_method(request_user_callback, "user", reify=True)
120 | config.add_request_method(request_ldapauth_callback, "ldapsource", reify=True)
121 |
122 | config.add_request_method(
123 | lambda: check_if_production(settings), "is_production", reify=True
124 | )
125 | setup_cors(config)
126 |
--------------------------------------------------------------------------------
/adaero/static/assets/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/adaero/static/assets/favicon-16x16.png
--------------------------------------------------------------------------------
/adaero/static/assets/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/adaero/static/assets/favicon-32x32.png
--------------------------------------------------------------------------------
/adaero/static/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/adaero/static/assets/logo.png
--------------------------------------------------------------------------------
/adaero/templates/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/adaero/templates/__init__.py
--------------------------------------------------------------------------------
/adaero/templates/email/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/adaero/templates/email/__init__.py
--------------------------------------------------------------------------------
/adaero/templates/email/approve.html.j2:
--------------------------------------------------------------------------------
1 | {% extends 'email/base.html.j2' %}
2 | {% block content %}
3 |
4 | Hi {{ user.first_name }},
5 |
6 | We have now reached the end of this feedback period and you have direct report(s) who have requested feedback
7 | on how they are performing. You should review and summarise feedback at {{ app_host }}/team-feedback
8 | (please open with Google Chrome) by {{ period.approval_end_utc | datetimeformat(user) }}.
9 |
10 | Remember that feedback should motivate, improve performance and support continuous learning, so consider how the
11 | feedback you are sharing does this.
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/adaero/templates/email/base.html.j2:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {% block content %}
11 | {% endblock %}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/adaero/templates/email/enrol.html.j2:
--------------------------------------------------------------------------------
1 | {% extends 'email/base.html.j2' %}
2 | {% block content %}
3 |
4 | Hi {{ user.first_name }},
5 |
6 | How well are you performing? How well do you understand your strengths and areas of improvement? Are there things you
7 | could do to improve your performance?
8 |
9 | If now would be a useful time to receive feedback from your colleagues, then enrol in the next
10 | feedback period here: {{ app_host }}/self-nominate
11 | (please open with Google Chrome) by {{ period.entry_start_utc | datetimeformat(user) }}.
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/adaero/templates/email/entry.html.j2:
--------------------------------------------------------------------------------
1 | {% extends 'email/base.html.j2' %}
2 | {% block content %}
3 |
4 | Hi {{ user.first_name }},
5 |
6 | Some of your colleagues would like to receive feedback on how they are performing. You are able to give feedback about anyone that has enrolled. You should prioritise people that you regularly work with.
7 | 1. You can give feedback to your colleagues here: {{ app_host }}/feedback (please open with Google Chrome).
8 | 2. If you have requested feedback for this period and would like to receive feedback from outside {{ business_unit }} (but within {{ company_name}}), you can invite them here: {{ app_host }}/invite-outside-reviewers .
9 |
10 | Guidelines:
11 | 1. There are only a few simple questions. You can leave as much or as little feedback as you choose to.
12 | 2. Everyone should contribute to improving the performance of their colleagues, so we recommend you invest time to give feedback to colleagues you have worked with in recent months.
13 |
3. Remember that feedback should motivate, improve performance and support continuous learning. Please consider this when providing feedback.
14 |
15 | The "Give feedback" period will close by {{ period.approval_start_utc | datetimeformat(user) }}.
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/adaero/templates/email/invite.html.j2:
--------------------------------------------------------------------------------
1 | {% extends 'email/base.html.j2' %}
2 | {% block content %}
3 |
4 | Hi {{ invitee.first_name }},
5 |
6 | You have been invited by {{ inviter.display_name }} from {{ inviter.business_unit }} to give feedback on their performance
7 | over the recent months. You can give them feedback here: {{ app_host }}/feedback/{{ inviter.username }} (please open with Google Chrome).
8 |
9 | Guidelines:
10 | 1. There are only a few simple questions. You can leave as much or as little feedback as you choose to.
11 | 2. Remember that feedback should motivate, improve performance and support continuous learning. Please consider this when providing feedback.
12 |
13 | The "Give feedback" period will close by {{ period.approval_start_utc | datetimeformat(invitee) }}.
14 | {% endblock %}
--------------------------------------------------------------------------------
/adaero/templates/email/review.html.j2:
--------------------------------------------------------------------------------
1 | {% extends 'email/base.html.j2' %}
2 | {% block content %}
3 |
4 | Hi {{ user.first_name }},
5 |
6 | Thank you for requesting feedback. You can access your feedback here: {{ app_host }}/feedback-about-me (please open with Google Chrome). Some things to consider that will help you get the most value from your feedback are:
7 | * Schedule time to debrief with your manager
8 | * Identify the areas you are motivated to work on and put a plan in place to act on the feedback
9 | * Remember your strengths, these are often your differentiators so you should find opportunities to use them
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/adaero/templates/email/tm_confirmation.html.j2:
--------------------------------------------------------------------------------
1 | {% extends 'email/base.html.j2' %}
2 | {% block content %}
3 |
4 | Hi {{ talent_manager.first_name }},
5 |
6 | This is an automated response from the 360 Feedback system. Emails with the subject "{{ subject }}" were sent out to {{ num_emails }} email addresses at {{ datetime_sent_utc | datetimeformat(talent_manager) }}.
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/adaero/text.py:
--------------------------------------------------------------------------------
1 | from copy import copy
2 | from datetime import datetime
3 |
4 | from adaero.constants import ANSWER_CHAR_LIMIT
5 |
6 |
7 | def check_input(input_):
8 | utcstr = datetime.utcnow()
9 | if len(input_) > ANSWER_CHAR_LIMIT:
10 | return (
11 | u"%s: Character limit of %s has been exceeded by %s. Please "
12 | u"reduce your answer size."
13 | % (utcstr, ANSWER_CHAR_LIMIT, len(input_) - ANSWER_CHAR_LIMIT)
14 | )
15 | return None
16 |
17 |
18 | def interpolate_template(template, **kwargs):
19 | data = copy(template)
20 | for key in data.keys():
21 | if isinstance(template[key], str):
22 | data[key] = template[key].format(**kwargs)
23 | return data
24 |
--------------------------------------------------------------------------------
/adaero/views/__init__.py:
--------------------------------------------------------------------------------
1 | from pyramid.static import static_view
2 | from pyramid.security import DENY_ALL
3 |
4 | static_view = static_view("adaero:static", use_subpath=True)
5 |
6 | # Safety net in case a view does not inherit from Root
7 | __acl__ = [DENY_ALL]
8 |
9 |
10 | class Root(object):
11 | __acl__ = [DENY_ALL]
12 |
13 |
14 | def includeme(config):
15 | """Pyramid convention that allows invocation of a function prior to
16 | server start and is found through `config.scan` in the main function"""
17 | config.add_route("catchall_static", "/*subpath")
18 | config.add_view("adaero.views.static_view", route_name="catchall_static")
19 |
--------------------------------------------------------------------------------
/adaero/views/auth.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from pyramid.httpexceptions import HTTPUnauthorized, HTTPOk
4 | from pyramid.security import (
5 | NO_PERMISSION_REQUIRED,
6 | forget,
7 | remember,
8 | Allow,
9 | Authenticated,
10 | )
11 |
12 | from logging import getLogger as get_logger
13 | from rest_toolkit import resource
14 |
15 | from adaero.constants import ALLOW_PASSWORDLESS_ACCESS_KEY, BUSINESS_UNIT_KEY
16 | from adaero.config import get_config_value
17 | from adaero.security import ldapauth, ANGULAR_2_XSRF_TOKEN_COOKIE_NAME
18 | from adaero.views import Root
19 |
20 |
21 | log = get_logger(__name__)
22 |
23 |
24 | def _build_user_data_response(request, username):
25 | request.response.status_int = 200
26 | request.response.set_cookie(
27 | ANGULAR_2_XSRF_TOKEN_COOKIE_NAME, request.session.get_csrf_token()
28 | )
29 | unit_name = get_config_value(request.registry.settings, BUSINESS_UNIT_KEY)
30 | return {
31 | "success": True,
32 | "data": {
33 | "displayName": request.user.display_name,
34 | "title": request.user.position,
35 | "principals": request.effective_principals,
36 | "businessUnit": unit_name,
37 | },
38 | }
39 |
40 |
41 | @resource("/api/v1/login")
42 | class Login(Root):
43 | def __init__(self, request): # pylint disable=unused-argument
44 | pass
45 |
46 |
47 | @Login.POST(permission=NO_PERMISSION_REQUIRED, require_csrf=False)
48 | def login(request):
49 | username = request.json_body["username"]
50 | password = request.json_body["password"]
51 | if not get_config_value(
52 | request.registry.settings, ALLOW_PASSWORDLESS_ACCESS_KEY
53 | ) and not request.ldapsource.auth_user(username, password):
54 | raise HTTPUnauthorized
55 | remember(request, username)
56 | return _build_user_data_response(request, username)
57 |
58 |
59 | @resource("/api/v1/user-data")
60 | class UserData(Root):
61 |
62 | __acl__ = [(Allow, Authenticated, "read")]
63 |
64 | def __init__(self, request): # pylint disable=unused-argument
65 | pass
66 |
67 |
68 | @UserData.GET(permission="read")
69 | def get_user_data(request):
70 | username = request.authenticated_userid
71 | if not username:
72 | raise HTTPUnauthorized
73 | return _build_user_data_response(request, username)
74 |
75 |
76 | @resource("/api/v1/logout")
77 | class Logout(Root):
78 | def __init__(self, request): # pylint disable=unused-argument
79 | pass
80 |
81 |
82 | @Logout.POST(permission=NO_PERMISSION_REQUIRED, require_csrf=False)
83 | def logout(request):
84 | headers = forget(request)
85 | return HTTPOk(headers=headers)
86 |
--------------------------------------------------------------------------------
/adaero/views/error.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from pyramid.httpexceptions import HTTPBadRequest, HTTPInternalServerError
4 | from pyramid.view import view_config, notfound_view_config, forbidden_view_config
5 |
6 | from logging import getLogger as get_logger
7 |
8 | log = get_logger(__name__)
9 |
10 |
11 | @view_config(context=HTTPBadRequest, renderer="json")
12 | def bad_request(exc, request):
13 | request.response.status_code = 400
14 | return {"message": exc.explanation}
15 |
16 |
17 | @view_config(context=HTTPInternalServerError, renderer="json")
18 | def internal_error(exc, request):
19 | request.response.status_code = 400
20 | if getattr(exc, "explanation"):
21 | message = exc.explanation
22 | else:
23 | message = "%s - Internal error on server" % datetime.utcnow()
24 | return {"message": message}
25 |
26 |
27 | @notfound_view_config(renderer="json")
28 | def not_found(exc, request):
29 | log.info("Page Not Found: %s", request.path_url)
30 | request.response.status_code = 404
31 | return {"message": exc.explanation}
32 |
33 |
34 | @forbidden_view_config(renderer="json")
35 | def forbidden(request):
36 | if request.unauthenticated_userid:
37 | log.warning("Forbidden: %s for %s", request.path_url, request.user.username)
38 | request.response.status_int = 403
39 | payload = {"message": "You are not allowed to perform this action"}
40 | if request.registry.settings.get("adaero.debug_all"):
41 | payload["user"] = request.user.to_dict()
42 | return payload
43 | else:
44 | request.response.status_int = 401
45 | return {"message": "You must login to perform this action"}
46 |
--------------------------------------------------------------------------------
/adaero/views/feedback.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from logging import getLogger as get_logger
4 | from pyramid.httpexceptions import HTTPNotFound
5 | from pyramid.security import Allow, Authenticated
6 | from rest_toolkit import resource
7 | from sqlalchemy.orm import joinedload
8 |
9 | from adaero import constants
10 | from adaero.config import get_config_value
11 | from adaero.forms import build_feedback_payload, update_feedback_form
12 | from adaero.models import ExternalInvite, FeedbackForm, Nominee, Period
13 | from adaero.history import fetch_feedback_history
14 | from adaero.security import EMPLOYEE_ROLE, EXTERNAL_BUSINESS_UNIT_ROLE
15 | from adaero.views import Root
16 |
17 | log = get_logger(__name__)
18 |
19 |
20 | @resource("/api/v1/feedback/{username:\w+}/")
21 | class FeedbackFormResource(Root):
22 |
23 | __acl__ = [(Allow, Authenticated, ("read", "update"))]
24 |
25 | def __init__(self, request): # pylint disable=unused-argument
26 | """Pre-check that the `request.user` is allowed to give feedback
27 | to `request.matchdict['username']`."""
28 | location = get_config_value(
29 | request.registry.settings, constants.HOMEBASE_LOCATION_KEY
30 | )
31 | self.current_period = Period.get_current_period(
32 | request.dbsession,
33 | options=(joinedload("template").joinedload("rows").joinedload("question")),
34 | )
35 |
36 | self.current_nominees = (
37 | request.dbsession.query(Nominee)
38 | .options(joinedload("user"))
39 | .filter(Nominee.period == self.current_period)
40 | )
41 |
42 | if self.current_period.subperiod(location) != Period.ENTRY_SUBPERIOD:
43 | raise HTTPNotFound(explanation="Currently not in the entry " "period.")
44 |
45 | self.to_username = request.matchdict["username"]
46 | self.from_username = request.user.username
47 |
48 | if self.to_username == self.from_username:
49 | raise HTTPNotFound(explanation="Cannot use feedback on self.")
50 |
51 | self.nominee = self.current_nominees.filter(
52 | Nominee.username == self.to_username
53 | ).one_or_none()
54 |
55 | if not self.nominee:
56 | raise HTTPNotFound(
57 | explanation='Nominee "%s" does not exist.' % self.to_username
58 | )
59 |
60 | if EXTERNAL_BUSINESS_UNIT_ROLE in request.effective_principals:
61 | exists = (
62 | request.dbsession.query(ExternalInvite)
63 | .filter(
64 | ExternalInvite.from_username == self.to_username,
65 | ExternalInvite.to_username == self.from_username,
66 | ExternalInvite.period_id == self.current_period.id,
67 | )
68 | .one_or_none()
69 | )
70 | if not exists:
71 | raise HTTPNotFound(
72 | explanation='User "%s" did not invite you '
73 | "for feedback." % self.to_username
74 | )
75 |
76 | self.form = (
77 | request.dbsession.query(FeedbackForm)
78 | .options(joinedload("answers").joinedload("question"))
79 | .filter(
80 | FeedbackForm.period_id == self.current_period.id,
81 | FeedbackForm.to_username == self.to_username,
82 | FeedbackForm.from_username == self.from_username,
83 | FeedbackForm.is_summary == False,
84 | ) # noqa
85 | .one_or_none()
86 | )
87 |
88 |
89 | @FeedbackFormResource.GET(permission="read")
90 | def view_feedback(context, request):
91 | return {"form": build_feedback_payload(context, request, False)}
92 |
93 |
94 | @FeedbackFormResource.PUT(permission="update")
95 | def put_feedback(context, request):
96 | return update_feedback_form(context, request, False)
97 |
98 |
99 | @resource("/api/v1/feedback-about-me")
100 | class FeedbackAboutMeResource(Root):
101 |
102 | __acl__ = [(Allow, EMPLOYEE_ROLE, "read")]
103 |
104 | def __init__(self, request): # pylint disable=unused-argument
105 | pass
106 |
107 |
108 | @FeedbackAboutMeResource.GET(permission="read")
109 | def get_feedback_about_me(_, request):
110 | return fetch_feedback_history(
111 | request.dbsession,
112 | request.user.username,
113 | request.registry.settings,
114 | fetch_full=True,
115 | )
116 |
--------------------------------------------------------------------------------
/adaero/views/metadata.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from pyramid.security import NO_PERMISSION_REQUIRED
4 | from pyramid.httpexceptions import HTTPFound
5 | from rest_toolkit import resource
6 |
7 | from adaero import constants
8 | from adaero.config import get_config_value
9 | from adaero.views import Root
10 |
11 |
12 | @resource("/api/v1/metadata")
13 | class Metadata(Root):
14 | def __init__(self, request): # pylint disable=unused-argument
15 | pass
16 |
17 |
18 | @Metadata.GET(permission=NO_PERMISSION_REQUIRED)
19 | def get_metadata(request):
20 | """
21 | Return data that can be used to personalize the current user's UI
22 | """
23 | is_pwl_access = bool(
24 | get_config_value(
25 | request.registry.settings, constants.ALLOW_PASSWORDLESS_ACCESS_KEY
26 | )
27 | )
28 | unit_name = get_config_value(request.registry.settings, constants.BUSINESS_UNIT_KEY)
29 | login_password_message = get_config_value(
30 | request.registry.settings, constants.LOGIN_PASSWORD_MSG_KEY
31 | )
32 | login_username_message = get_config_value(
33 | request.registry.settings, constants.LOGIN_USERNAME_MSG_KEY
34 | )
35 | support_email = get_config_value(
36 | request.registry.settings, constants.SUPPORT_EMAIL_KEY
37 | )
38 | display_name = get_config_value(
39 | request.registry.settings, constants.COMPANY_NAME_KEY
40 | )
41 | return {
42 | "metadata": {
43 | "businessUnit": unit_name,
44 | "displayName": display_name,
45 | "loginPasswordMessage": login_password_message,
46 | "loginUsernameMessage": login_username_message,
47 | "passwordlessAccess": is_pwl_access,
48 | "supportEmail": support_email,
49 | }
50 | }
51 |
52 |
53 | @resource("/api/v1/logo.png")
54 | class Logo(Root):
55 | def __init__(self, request): # pylint disable=unused-argument
56 | pass
57 |
58 |
59 | @Logo.GET(permission=NO_PERMISSION_REQUIRED)
60 | def redirect_logo(request):
61 | logo_filename = get_config_value(
62 | request.registry.settings, constants.LOGO_FILENAME_KEY, "logo.png"
63 | )
64 | is_https = get_config_value(
65 | request.registry.settings, constants.SERVED_ON_HTTPS_KEY
66 | )
67 | raise HTTPFound(
68 | request.route_url(
69 | "catchall_static",
70 | subpath="assets/%s" % str(logo_filename),
71 | _scheme="https" if is_https else None,
72 | )
73 | )
74 |
--------------------------------------------------------------------------------
/architecture.dot:
--------------------------------------------------------------------------------
1 | strict graph {
2 | {
3 | "Web Frontend" [shape=box]
4 | "API Backend" [shape=box]
5 | "Database" [shape=cylinder]
6 | "LDAP" [shape=cylinder]
7 | "User" [shape=point]
8 | }
9 | "API Backend" -- "Database"
10 | "API Backend" -- "LDAP" [label="Only for authentication, NOT authorization"]
11 | "User" -- "Web Frontend"
12 | "Web Frontend" -- "API Backend"
13 | }
--------------------------------------------------------------------------------
/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/architecture.png
--------------------------------------------------------------------------------
/docker/dev/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2.4'
2 | services:
3 | webserver:
4 | image: adaero-dev-frontend:latest
5 | volumes:
6 | - ../../frontend/src:/opt/app-root/src/src
7 | ports:
8 | - "4200:4200"
9 | networks:
10 | - web
11 | depends_on:
12 | - webapp
13 | healthcheck:
14 | test: ["CMD-SHELL", "wget http://localhost:4200"]
15 | interval: 5s
16 | timeout: 5s
17 | retries: 3
18 | logging:
19 | driver: "none"
20 | webapp:
21 | image: adaero-app:latest
22 | networks:
23 | - web
24 | - db
25 | - ldap
26 | volumes:
27 | - ../../:/opt/app-root/src
28 | depends_on:
29 | db:
30 | condition: service_healthy
31 | ldap:
32 | condition: service_healthy
33 | db:
34 | image: postgres:12
35 | environment:
36 | - POSTGRES_PASSWORD=mysecretpassword
37 | ports:
38 | - "5432:5432"
39 | networks:
40 | - db
41 | healthcheck:
42 | test: ["CMD-SHELL", "psql -U postgres -c \"select 'OK';\""]
43 | interval: 5s
44 | timeout: 5s
45 | retries: 3
46 | logging:
47 | driver: "none"
48 | ldap:
49 | image: osixia/openldap:latest
50 | volumes:
51 | - ../shared/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom
52 | networks:
53 | - ldap
54 | ports:
55 | - "389:389"
56 | healthcheck:
57 | test: ["CMD-SHELL", "ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D \"cn=admin,dc=example,dc=org\" -w admin"]
58 | interval: 5s
59 | timeout: 5s
60 | retries: 3
61 | command: --copy-service
62 | ldap-ui:
63 | image: osixia/phpldapadmin:latest
64 | environment:
65 | - PHPLDAPADMIN_LDAP_HOSTS=ldap
66 | ports:
67 | - "6443:443"
68 | networks:
69 | - ldap
70 | depends_on:
71 | - ldap
72 | logging:
73 | driver: "none"
74 |
75 | networks:
76 | ldap:
77 | web:
78 | db:
79 |
80 |
81 |
--------------------------------------------------------------------------------
/docker/prod/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2.4'
2 | services:
3 | webserver:
4 | image: nginx
5 | volumes:
6 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
7 | - ./nginx/adaero.conf:/etc/nginx/sites-enabled/adaero.conf:ro
8 | ports:
9 | - "8080:8080"
10 | networks:
11 | - web
12 | depends_on:
13 | - webapp
14 | webapp:
15 | image: adaero-app:latest
16 | networks:
17 | - web
18 | - db
19 | - ldap
20 | depends_on:
21 | db:
22 | condition: service_healthy
23 | ldap:
24 | condition: service_healthy
25 | db:
26 | image: postgres:12
27 | environment:
28 | - POSTGRES_PASSWORD=mysecretpassword
29 | ports:
30 | - "5432:5432"
31 | networks:
32 | - db
33 | healthcheck:
34 | test: ["CMD-SHELL", "psql -U postgres -c \"select 'OK';\""]
35 | interval: 5s
36 | timeout: 5s
37 | retries: 3
38 | ldap:
39 | image: osixia/openldap:latest
40 | volumes:
41 | # - ./ldif:/container/service/slapd/assets/config/bootstrap/ldif
42 | - ../shared/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom
43 | networks:
44 | - ldap
45 | ports:
46 | - "389:389"
47 | healthcheck:
48 | test: ["CMD-SHELL", "ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D \"cn=admin,dc=example,dc=org\" -w admin"]
49 | interval: 5s
50 | timeout: 5s
51 | retries: 3
52 | command: --copy-service --loglevel debug
53 | ldap-ui:
54 | image: osixia/phpldapadmin:latest
55 | environment:
56 | - PHPLDAPADMIN_LDAP_HOSTS=ldap
57 | ports:
58 | - "6443:443"
59 | networks:
60 | - ldap
61 | depends_on:
62 | - ldap
63 |
64 | networks:
65 | ldap:
66 | web:
67 | db:
68 |
69 |
70 |
--------------------------------------------------------------------------------
/docker/prod/nginx/adaero.conf:
--------------------------------------------------------------------------------
1 | upstream webapp-site {
2 | server webapp:8080;
3 | }
4 |
5 | server {
6 | listen 8080;
7 |
8 | location / {
9 | proxy_set_header Host $http_host;
10 | proxy_set_header X-Real-IP $remote_addr;
11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
12 | proxy_set_header X-Forwarded-Proto $scheme;
13 |
14 | client_max_body_size 10m;
15 | client_body_buffer_size 128k;
16 | proxy_connect_timeout 60s;
17 | proxy_send_timeout 90s;
18 | proxy_read_timeout 90s;
19 | proxy_buffering off;
20 | proxy_temp_file_write_size 64k;
21 | proxy_pass http://webapp-site;
22 | proxy_redirect off;
23 | }
24 | }
--------------------------------------------------------------------------------
/docker/prod/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | # nginx.conf
2 |
3 | user www-data;
4 | worker_processes 4;
5 | pid /var/run/nginx.pid;
6 |
7 | events {
8 | worker_connections 1024;
9 | # multi_accept on;
10 | }
11 |
12 | http {
13 |
14 | ##
15 | # Basic Settings
16 | ##
17 |
18 | sendfile on;
19 | tcp_nopush on;
20 | tcp_nodelay on;
21 | keepalive_timeout 65;
22 | types_hash_max_size 2048;
23 | # server_tokens off;
24 |
25 | # server_names_hash_bucket_size 64;
26 | # server_name_in_redirect off;
27 |
28 | include /etc/nginx/mime.types;
29 | default_type application/octet-stream;
30 |
31 | ##
32 | # Logging Settings
33 | ##
34 |
35 | access_log /var/log/nginx/access.log;
36 | error_log /var/log/nginx/error.log;
37 |
38 | ##
39 | # Gzip Settings
40 | ##
41 |
42 | gzip on;
43 | gzip_disable "msie6";
44 |
45 | ##
46 | # Virtual Host Configs
47 | ##
48 |
49 | include /etc/nginx/conf.d/*.conf;
50 | include /etc/nginx/sites-enabled/*;
51 | }
--------------------------------------------------------------------------------
/docker/shared/ldif/01-data.ldif:
--------------------------------------------------------------------------------
1 | dn: ou=People,dc=example,dc=org
2 | changetype: add
3 | objectClass: organizationalunit
4 | objectClass: top
5 | ou: People
6 |
7 | dn: uid=alovelace,ou=People,dc=example,dc=org
8 | changetype: add
9 | objectClass: person
10 | objectClass: inetOrgPerson
11 | objectClass: organizationalPerson
12 | objectClass: posixAccount
13 | objectClass: top
14 | uid: alovelace
15 | homeDirectory: /home/alovelace
16 | userpassword: password
17 | givenname: Ada
18 | cn: Ada Lovelace
19 | sn: Lovelace
20 | mail: alovelace@example.com
21 | l: London
22 | ou: Payroll
23 | ou: People
24 | uidNumber: 4000
25 | gidNumber: 1000
26 | o: Engineering
27 | departmentNumber: 123
28 | title: Software Engineer I
29 | manager: uid=dthomas,ou=People,dc=example,dc=org
30 |
31 | dn: uid=bsmith,ou=People,dc=example,dc=org
32 | changetype: add
33 | objectClass: person
34 | objectClass: inetOrgPerson
35 | objectClass: organizationalPerson
36 | objectClass: posixAccount
37 | objectClass: top
38 | uid: bsmith
39 | homeDirectory: /home/bsmith
40 | userpassword: password
41 | givenname: Bob
42 | cn: Bob Smith
43 | sn: Smith
44 | mail: bsmith@example.com
45 | l: Boston
46 | ou: Product Testing
47 | ou: People
48 | uidNumber: 5000
49 | gidNumber: 1000
50 | o: Engineering
51 | departmentNumber: 123
52 | title: Software Engineer II
53 | manager: uid=dthomas,ou=People,dc=example,dc=org
54 |
55 | dn: uid=cdalton,ou=People,dc=example,dc=org
56 | changetype: add
57 | objectClass: person
58 | objectClass: inetOrgPerson
59 | objectClass: organizationalPerson
60 | objectClass: posixAccount
61 | objectClass: top
62 | uid: cdalton
63 | homeDirectory: /home/cdalton
64 | userpassword: password
65 | givenname: Charles
66 | cn: Charles Dalton
67 | sn: Dalton
68 | mail: cdalton@example.com
69 | l: London
70 | ou: Payroll
71 | ou: People
72 | uidNumber: 6000
73 | gidNumber: 1000
74 | o: Engineering
75 | departmentNumber: 123
76 | title: Software Engineer III
77 | manager: uid=dthomas,ou=People,dc=example,dc=org
78 |
79 | dn: uid=dthomas,ou=People,dc=example,dc=org
80 | changetype: add
81 | objectClass: person
82 | objectClass: inetOrgPerson
83 | objectClass: organizationalPerson
84 | objectClass: posixAccount
85 | objectClass: top
86 | uid: dthomas
87 | homeDirectory: /home/dthomas
88 | userpassword: password
89 | givenname: Dan
90 | cn: Dan Thomas
91 | sn: Thomas
92 | mail: dthomas@example.com
93 | l: London
94 | ou: Accounting
95 | ou: People
96 | uidNumber: 7000
97 | gidNumber: 1000
98 | o: Engineering
99 | departmentNumber: 123
100 | title: Team Leader
101 | manager: uid=eforshaw,ou=People,dc=example,dc=org
102 |
103 | dn: uid=eforshaw,ou=People,dc=example,dc=org
104 | changetype: add
105 | objectClass: person
106 | objectClass: inetOrgPerson
107 | objectClass: organizationalPerson
108 | objectClass: posixAccount
109 | objectClass: top
110 | uid: eforshaw
111 | homeDirectory: /home/eforshaw
112 | userpassword: password
113 | givenname: Elaine
114 | cn: Elaine Forshaw
115 | sn: Forshaw
116 | mail: eforshaw@example.com
117 | l: London
118 | ou: Accounting
119 | ou: People
120 | uidNumber: 8000
121 | gidNumber: 1000
122 | o: Engineering
123 | departmentNumber: 123
124 | title: Vice President
125 |
126 |
--------------------------------------------------------------------------------
/docs/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/docs/login.png
--------------------------------------------------------------------------------
/docs/user-guide.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/docs/user-guide.pdf
--------------------------------------------------------------------------------
/docs/welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/docs/welcome.png
--------------------------------------------------------------------------------
/frontend/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | testem.log
34 | /typings
35 |
36 | # e2e
37 | /e2e/*.js
38 | /e2e/*.map
39 |
40 | # System Files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10
2 |
3 | # Create app directory
4 | WORKDIR /usr/src/app
5 |
6 | # Install app dependencies
7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied
8 | # where available (npm@5+)
9 | COPY package*.json ./
10 |
11 | RUN npm install
12 | # If you are building your code for production
13 | # RUN npm ci --only=production
14 |
15 | # Bundle app source
16 | COPY . .
17 |
18 | EXPOSE 4200
19 | CMD [ "npm", "run", "start" ]
20 |
21 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # adaero-frontend
2 |
3 | ## Development server
4 |
5 | Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. All HTTP calls to `/api` are proxied, so please have the `adaero` Pyramid backend running in development mode.
6 |
7 | ## Code scaffolding
8 |
9 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
10 |
11 | ## Build
12 |
13 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
14 |
15 | ## Running unit tests
16 |
17 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
18 |
19 | ## Running end-to-end tests
20 |
21 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
22 |
23 | ## Further help
24 |
25 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
26 |
--------------------------------------------------------------------------------
/frontend/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "my-app": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "architect": {
11 | "build": {
12 | "builder": "@angular-devkit/build-angular:browser",
13 | "options": {
14 | "aot": true,
15 | "outputPath": "dist",
16 | "index": "src/index.html",
17 | "main": "src/main.ts",
18 | "tsConfig": "src/tsconfig.app.json",
19 | "polyfills": "src/polyfills.ts",
20 | "assets": [
21 | "src/assets",
22 | "src/favicon.ico"
23 | ],
24 | "styles": [
25 | "src/styles.scss"
26 | ],
27 | "scripts": []
28 | },
29 | "configurations": {
30 | "production": {
31 | "budgets": [
32 | {
33 | "type": "anyComponentStyle",
34 | "maximumWarning": "6kb"
35 | }
36 | ],
37 | "optimization": true,
38 | "outputHashing": "all",
39 | "sourceMap": false,
40 | "extractCss": true,
41 | "namedChunks": false,
42 | "aot": true,
43 | "extractLicenses": true,
44 | "vendorChunk": false,
45 | "buildOptimizer": true,
46 | "fileReplacements": [
47 | {
48 | "replace": "src/environments/environment.ts",
49 | "with": "src/environments/environment.prod.ts"
50 | }
51 | ]
52 | }
53 | }
54 | },
55 | "serve": {
56 | "builder": "@angular-devkit/build-angular:dev-server",
57 | "options": {
58 | "browserTarget": "my-app:build"
59 | },
60 | "configurations": {
61 | "production": {
62 | "browserTarget": "my-app:build:production"
63 | }
64 | }
65 | },
66 | "extract-i18n": {
67 | "builder": "@angular-devkit/build-angular:extract-i18n",
68 | "options": {
69 | "browserTarget": "my-app:build"
70 | }
71 | },
72 | "test": {
73 | "builder": "@angular-devkit/build-angular:karma",
74 | "options": {
75 | "main": "src/test.ts",
76 | "karmaConfig": "./karma.conf.js",
77 | "polyfills": "src/polyfills.ts",
78 | "tsConfig": "src/tsconfig.spec.json",
79 | "scripts": [],
80 | "styles": [
81 | "src/styles.scss"
82 | ],
83 | "assets": [
84 | "src/assets",
85 | "src/favicon.ico"
86 | ]
87 | }
88 | },
89 | "lint": {
90 | "builder": "@angular-devkit/build-angular:tslint",
91 | "options": {
92 | "tsConfig": [
93 | "src/tsconfig.app.json",
94 | "src/tsconfig.spec.json"
95 | ],
96 | "exclude": [
97 | "**/node_modules/**"
98 | ]
99 | }
100 | }
101 | }
102 | },
103 | "my-app-e2e": {
104 | "root": "e2e",
105 | "sourceRoot": "e2e",
106 | "projectType": "application",
107 | "architect": {
108 | "e2e": {
109 | "builder": "@angular-devkit/build-angular:protractor",
110 | "options": {
111 | "protractorConfig": "./protractor.conf.js",
112 | "devServerTarget": "my-app:serve"
113 | }
114 | },
115 | "lint": {
116 | "builder": "@angular-devkit/build-angular:tslint",
117 | "options": {
118 | "tsConfig": [
119 | "e2e/tsconfig.e2e.json"
120 | ],
121 | "exclude": [
122 | "**/node_modules/**"
123 | ]
124 | }
125 | }
126 | }
127 | }
128 | },
129 | "defaultProject": "my-app",
130 | "schematics": {
131 | "@schematics/angular:component": {
132 | "prefix": "app",
133 | "style": "scss"
134 | },
135 | "@schematics/angular:directive": {
136 | "prefix": "app"
137 | }
138 | },
139 | "cli": {
140 | "analytics": false
141 | }
142 | }
--------------------------------------------------------------------------------
/frontend/browserslist:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/frontend/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 |
3 | describe('my-app App', () => {
4 | let page: AppPage;
5 |
6 | beforeEach(() => {
7 | page = new AppPage();
8 | });
9 |
10 | it('should display welcome message', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('Welcome to app!');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/frontend/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('app-root h1')).getText();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "baseUrl": "./",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": [
9 | "jasmine",
10 | "jasminewd2",
11 | "node"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-firefox-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
20 | fixWebpackSourcePaths: true
21 | },
22 |
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Firefox'],
29 | singleRun: false
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adaero-frontend",
3 | "version": "0.0.0",
4 | "license": "AGPL 3.0",
5 | "scripts": {
6 | "ng": "ng",
7 | "start": "ng serve --watch --host 0.0.0.0 --disable-host-check --poll 1 --proxy-config proxy.conf.json",
8 | "local-dev-server": "ng serve --watch --host 0.0.0.0 --disable-host-check --proxy-config proxy.local.conf.json",
9 | "build": "ng build",
10 | "dev-dist": "ng build && cp -r dist/* ../adaero/static/",
11 | "prod-dist": "ng build --prod && cp -r dist/* ../adaero/static/",
12 | "test": "ng test",
13 | "lint": "ng lint",
14 | "e2e": "ng e2e"
15 | },
16 | "private": true,
17 | "dependencies": {
18 | "@angular-devkit/build-angular": "~0.901.0",
19 | "@angular/animations": "^9.1.0",
20 | "@angular/cli": "^9.1.0",
21 | "@angular/common": "^9.1.0",
22 | "@angular/compiler": "^9.1.0",
23 | "@angular/compiler-cli": "^9.1.0",
24 | "@angular/core": "^9.1.0",
25 | "@angular/forms": "^9.1.0",
26 | "@angular/localize": "^9.1.0",
27 | "@angular/platform-browser": "^9.1.0",
28 | "@angular/platform-browser-dynamic": "^9.1.0",
29 | "@angular/platform-server": "^9.1.0",
30 | "@angular/router": "^9.1.0",
31 | "@ng-bootstrap/ng-bootstrap": "^5.3.0",
32 | "bootstrap": "^4.3.0",
33 | "core-js": "^2.6.11",
34 | "jquery": "^1.9.1",
35 | "node-sass": "^4.13.1",
36 | "popper.js": "^1.16.1",
37 | "rxjs": "^6.5.5",
38 | "tslib": "^1.10.0",
39 | "zone.js": "~0.10.2"
40 | },
41 | "devDependencies": {
42 | "@angular/language-service": "^9.1.0",
43 | "@types/jasmine": "~2.5.53",
44 | "@types/jasminewd2": "^2.0.8",
45 | "@types/node": "^12.11.1",
46 | "codelyzer": "^5.1.2",
47 | "copy-webpack-plugin": "^4.6.0",
48 | "jasmine-core": "~2.6.2",
49 | "jasmine-spec-reporter": "~4.1.0",
50 | "karma": "~1.7.0",
51 | "karma-chrome-launcher": "~2.1.1",
52 | "karma-cli": "~1.0.1",
53 | "karma-coverage-istanbul-reporter": "^1.4.3",
54 | "karma-firefox-launcher": "^1.3.0",
55 | "karma-jasmine": "~1.1.0",
56 | "karma-jasmine-html-reporter": "^0.2.2",
57 | "protractor": "~5.1.2",
58 | "ts-node": "~3.2.0",
59 | "tslint": "~5.7.0",
60 | "typescript": "~3.8.3"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/frontend/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | onPrepare() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/frontend/proxy.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api": {
3 | "target": "http://webapp:8080",
4 | "secure": false
5 | },
6 | "/assets": {
7 | "target": "http://webapp:8080",
8 | "secure": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/proxy.local.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api": {
3 | "target": "http://localhost:8080",
4 | "secure": false
5 | },
6 | "/assets": {
7 | "target": "http://localhost:8080",
8 | "secure": false
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 |
4 | import {
5 | GiveFeedbackComponent,
6 | NomineesListComponent,
7 | LoginComponent,
8 | SelfNominateComponent,
9 | StatsComponent,
10 | SummariseFeedbackComponent,
11 | FeedbackAboutMeComponent,
12 | TalentManagerPanelComponent,
13 | AuthenticatedComponent,
14 | FeedbackHistoryViewComponent,
15 | ExternalInviteComponent
16 | } from './components/views';
17 |
18 | import { AuthGuardService, AnonGuardService, PendingChangesGuardService } from './guards';
19 |
20 | const routes: Routes = [
21 | { path: '', redirectTo: '/feedback', pathMatch: 'full' },
22 | { path: 'login', canActivate: [AnonGuardService], component: LoginComponent },
23 | {
24 | path: '',
25 | component: AuthenticatedComponent,
26 | canActivate: [AuthGuardService],
27 | children: [
28 | { path: 'self-nominate', component: SelfNominateComponent },
29 | { path: 'feedback-about-me', component: FeedbackAboutMeComponent },
30 | { path: 'feedback', component: NomineesListComponent },
31 | { path: 'invite-outside-reviewers', component: ExternalInviteComponent },
32 | {
33 | path: 'feedback/:username',
34 | canDeactivate: [PendingChangesGuardService],
35 | component: GiveFeedbackComponent
36 | },
37 | { path: 'team-feedback', component: StatsComponent, data: {isCompanyWide: false} },
38 | { path: 'company-feedback', component: StatsComponent, data: {isCompanyWide: true}},
39 | {
40 | path: 'team-feedback/:username/summarise',
41 | canDeactivate: [PendingChangesGuardService],
42 | component: SummariseFeedbackComponent
43 | },
44 | {
45 | path: 'team-feedback/:username/history',
46 | component: FeedbackHistoryViewComponent
47 | },
48 | { path: 'talent-manager-panel', component: TalentManagerPanelComponent }
49 |
50 | ]
51 | }
52 | ];
53 |
54 | @NgModule({
55 | imports: [RouterModule.forRoot(routes)],
56 | exports: [RouterModule]
57 | })
58 | export class AppRoutingModule { }
59 |
--------------------------------------------------------------------------------
/frontend/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/app/app.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/frontend/src/app/app.component.scss
--------------------------------------------------------------------------------
/frontend/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, async } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { HttpClientModule } from '@angular/common/http';
4 |
5 | import { AppComponent } from './app.component';
6 | import { ApiService } from './services/api.service';
7 | describe('AppComponent', () => {
8 | beforeEach(async(() => {
9 | TestBed.configureTestingModule({
10 | imports: [
11 | HttpClientModule,
12 | RouterTestingModule
13 | ],
14 | declarations: [
15 | AppComponent
16 | ],
17 | providers: [
18 | ApiService
19 | ]
20 | }).compileComponents();
21 | }));
22 | it('should create the app', async(() => {
23 | const fixture = TestBed.createComponent(AppComponent);
24 | const app = fixture.debugElement.componentInstance;
25 | expect(app).toBeTruthy();
26 | }));
27 | });
28 |
--------------------------------------------------------------------------------
/frontend/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import {ApiService, MetadataPayload, UserData} from './services/api.service';
3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
4 | import { ModalComponent } from './components/widgets';
5 | import { HttpErrorResponse } from '@angular/common/http';
6 |
7 | @Component({
8 | selector: 'app-root',
9 | templateUrl: './app.component.html',
10 | styleUrls: ['./app.component.scss']
11 | })
12 | export class AppComponent {
13 | constructor(public api: ApiService, public modal: NgbModal) {
14 | api.error$.subscribe(
15 | (error: HttpErrorResponse) => {
16 | const c = modal.open(ModalComponent);
17 | c.componentInstance.confirmText = 'OK';
18 | api.getMetadata().subscribe((result: MetadataPayload) => {
19 | c.componentInstance.dialog = `Sorry, an error has occured and the requested action has failed. If you think this should have worked,
20 | please email ${result.metadata.supportEmail} with the following information`;
21 | c.componentInstance.metadata = `${error.error.message ? error.error.message : error.message}`;
22 | return c.result.then(() => {
23 | return true;
24 | }, () => {
25 | return false;
26 | });
27 | });
28 | }
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { LOCALE_ID, NgModule } from '@angular/core';
3 | import { FormsModule } from '@angular/forms';
4 | import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
5 |
6 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
7 |
8 | import { AppComponent } from './app.component';
9 |
10 | import {
11 | GiveFeedbackComponent,
12 | NomineesListComponent,
13 | LoginComponent,
14 | SelfNominateComponent,
15 | StatsComponent,
16 | SummariseFeedbackComponent,
17 | FeedbackAboutMeComponent,
18 | TalentManagerPanelComponent,
19 | } from './components/views';
20 |
21 | import {
22 | FeedbackFormComponent,
23 | } from './components/widgets/';
24 |
25 | import { AppRoutingModule } from './app-routing.module';
26 |
27 | import { ApiService } from './services/api.service';
28 | import { CookieService } from './services/cookie.service';
29 | import { AuthGuardService, AnonGuardService, PendingChangesGuardService } from './guards';
30 | import { NomineeFilterPipe } from './pipes/nominee-filter.pipe';
31 | import { ModalComponent } from './components/widgets/modal/modal.component';
32 | import { AuthenticatedComponent } from './components/views/authenticated/authenticated.component';
33 | import { FeedbackHistoryComponent } from './components/widgets/feedback-history/feedback-history.component';
34 | import { FeedbackHistoryViewComponent } from './components/views/feedback-history-view/feedback-history-view.component';
35 | import { ExternalInviteComponent } from './components/views/external-invite/external-invite.component';
36 |
37 | @NgModule({
38 | declarations: [
39 | // views
40 | AppComponent,
41 | LoginComponent,
42 | AuthenticatedComponent,
43 | GiveFeedbackComponent,
44 | NomineesListComponent,
45 | SelfNominateComponent,
46 | StatsComponent,
47 | SummariseFeedbackComponent,
48 | FeedbackAboutMeComponent,
49 | TalentManagerPanelComponent,
50 | FeedbackHistoryViewComponent,
51 | ExternalInviteComponent,
52 |
53 | // components
54 | FeedbackFormComponent,
55 | ModalComponent,
56 | FeedbackHistoryComponent,
57 |
58 | // pipes
59 | NomineeFilterPipe,
60 |
61 |
62 |
63 | ],
64 | entryComponents: [
65 | ModalComponent,
66 | ],
67 | imports: [
68 | BrowserModule,
69 | HttpClientModule,
70 | HttpClientXsrfModule,
71 | FormsModule,
72 | NgbModule,
73 | // local modules
74 | AppRoutingModule,
75 | ],
76 | providers: [
77 | ApiService,
78 | AnonGuardService,
79 | AuthGuardService,
80 | PendingChangesGuardService,
81 | CookieService,
82 | { provide: LOCALE_ID, useValue: 'en'}
83 | ],
84 | bootstrap: [AppComponent]
85 | })
86 | export class AppModule { }
87 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/authenticated/authenticated.component.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
{{ user.displayName }}
13 |
{{ user.title }}
14 |
Logout
15 |
16 |
17 |
49 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/authenticated/authenticated.component.scss:
--------------------------------------------------------------------------------
1 | @import '~styles.scss';
2 |
3 | .wrapper {
4 | border-right: 0.1em solid black;
5 | }
6 |
7 | #sidebar {
8 | padding: 0.5em;
9 | height: 88vh;
10 | .nav-pills, .nav-link {
11 | border-radius: 0em;
12 | color: black;
13 | }
14 | .active {
15 | background-color: $grey;
16 | font-weight: bold;
17 | }
18 | }
19 |
20 | #content {
21 | padding: 1em;
22 | height: 100%;
23 | }
24 |
25 | #logged-in-user-info {
26 | padding: 0em 0.5em;
27 | color: white;
28 | text-align: right;
29 | }
30 |
31 | .nav > li > a {
32 | color: black;
33 | }
34 |
35 | .nav > li > a:hover, .nav > li > a:focus {
36 | font-weight: bold;
37 | }
38 |
39 | @media print {
40 | nav, .wrapper {
41 | display: none;
42 | }
43 | }
--------------------------------------------------------------------------------
/frontend/src/app/components/views/authenticated/authenticated.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { AuthenticatedComponent } from './authenticated.component';
4 |
5 | describe('AuthenticatedComponent', () => {
6 | let component: AuthenticatedComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ AuthenticatedComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(AuthenticatedComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/authenticated/authenticated.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ApiService, UserData, MetadataPayload, Metadata } from '../../../services/api.service';
3 |
4 | @Component({
5 | selector: 'app-authenticated',
6 | templateUrl: './authenticated.component.html',
7 | styleUrls: ['./authenticated.component.scss']
8 | })
9 | export class AuthenticatedComponent implements OnInit {
10 |
11 | user: UserData;
12 | public metadata: Metadata;
13 | constructor(private api: ApiService) { }
14 |
15 | ngOnInit() {
16 | this.fetchData();
17 | }
18 |
19 | fetchData() {
20 | this.api.getUserData().subscribe((result) => {
21 | this.user = result;
22 | });
23 | this.api.getMetadata().subscribe(
24 | (result: MetadataPayload) => {
25 | this.metadata = result.metadata;
26 | },
27 | );
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/external-invite/external-invite.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Invite additional reviewers
5 |
6 | You are welcome to invite reviewers from across {{ this.metadata.companyName }} by entering their email address below. An email will be sent, asking them to give you feedback.
7 |
8 |
9 |
10 | {{ errorMsg }}
11 |
12 |
13 |
14 |
15 | {{ successMsg }}
16 |
17 |
18 | 0">
19 | Your existing invites:
20 |
21 | {{ invitee.displayName }} <{{ invitee.email }}>, {{ invitee.businessUnit }}, {{ invitee.department }}
22 |
23 | If you wish to send a reminder email, then simply re-enter their email address below.
24 |
25 |
33 |
34 |
35 | {{ this.status.heading }}
36 | {{ this.status.body }}
37 |
38 |
39 |
40 | Loading Data...
41 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/external-invite/external-invite.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/frontend/src/app/components/views/external-invite/external-invite.component.scss
--------------------------------------------------------------------------------
/frontend/src/app/components/views/external-invite/external-invite.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ExternalInviteComponent } from './external-invite.component';
4 |
5 | describe('ExternalInviteComponent', () => {
6 | let component: ExternalInviteComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ ExternalInviteComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(ExternalInviteComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/external-invite/external-invite.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewChild } from '@angular/core';
2 | import { NgForm } from '@angular/forms';
3 | import { ApiService, ExternalFeedbackStatusPayload, MetadataPayload, Metadata } from '../../../services/api.service';
4 |
5 | @Component({
6 | selector: 'app-external-invite',
7 | templateUrl: './external-invite.component.html',
8 | styleUrls: ['./external-invite.component.scss']
9 | })
10 | export class ExternalInviteComponent implements OnInit {
11 |
12 | successMsg: string;
13 | errorMsg: string;
14 | metadata: Metadata;
15 | status: ExternalFeedbackStatusPayload | {};
16 | constructor(private api: ApiService) { }
17 |
18 | ngOnInit() {
19 | this.fetchStatus();
20 | this.api.getMetadata().subscribe(
21 | (result: MetadataPayload) => {
22 | this.metadata = result.metadata;
23 | },
24 | );
25 | }
26 |
27 | fetchStatus() {
28 | this.api.getExternalInviteStatus().subscribe((payload) => {
29 | this.status = payload;
30 | });
31 | }
32 |
33 | onSubmit(form: NgForm) {
34 | this.successMsg = null;
35 | this.errorMsg = null;
36 | this.api.sendExternalInvite(form.value.email).subscribe(() => {
37 | this.successMsg = 'Invite successfully sent!';
38 | }, (error) => {
39 | this.errorMsg = error.error ? error.error.message : error.message;
40 | });
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/feedback-about-me/feedback-about-me.component.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/feedback-about-me/feedback-about-me.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/frontend/src/app/components/views/feedback-about-me/feedback-about-me.component.scss
--------------------------------------------------------------------------------
/frontend/src/app/components/views/feedback-about-me/feedback-about-me.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { FeedbackAboutMeComponent } from './feedback-about-me.component';
4 |
5 | describe('FeedbackAboutMeComponent', () => {
6 | let component: FeedbackAboutMeComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ FeedbackAboutMeComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(FeedbackAboutMeComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/feedback-about-me/feedback-about-me.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ApiService, FeedbackHistoryPayload } from '../../../services/api.service';
3 |
4 | @Component({
5 | selector: 'app-feedback-about-me',
6 | templateUrl: './feedback-about-me.component.html',
7 | styleUrls: ['./feedback-about-me.component.scss']
8 | })
9 | export class FeedbackAboutMeComponent implements OnInit {
10 |
11 | data: FeedbackHistoryPayload | {};
12 | constructor(public api: ApiService) { }
13 |
14 | readonly title = 'Review my feedback';
15 | readonly description = [`Find below a summary of your feedback. Your colleagues were given the opportunity to provide feedback. \
16 | Your Line Manager then reviewed the feedback and provided the summary presented below.`,
17 | `Managers can view historical feedback about their Direct Reports spanning the last three feedback cycles. \
18 | Staff can see their full history of feedback.`,
19 | `If you would like to discuss your feedback you should reach out to either your Line Manager or your Talent Manager.`];
20 |
21 | ngOnInit() {
22 | this.api.getFeedbackAboutMe().subscribe(data => {
23 | this.data = data;
24 | });
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/feedback-history-view/feedback-history-view.component.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/feedback-history-view/feedback-history-view.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/frontend/src/app/components/views/feedback-history-view/feedback-history-view.component.scss
--------------------------------------------------------------------------------
/frontend/src/app/components/views/feedback-history-view/feedback-history-view.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { FeedbackHistoryViewComponent } from './feedback-history-view.component';
4 |
5 | describe('FeedbackHistoryViewComponent', () => {
6 | let component: FeedbackHistoryViewComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ FeedbackHistoryViewComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(FeedbackHistoryViewComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/feedback-history-view/feedback-history-view.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { ActivatedRoute } from '@angular/router';
3 | import { ApiService, FeedbackHistoryPayload } from '../../../services/api.service';
4 | import { Observable } from 'rxjs';
5 | import { map } from 'rxjs/operators';
6 |
7 | @Component({
8 | selector: 'app-feedback-history-view',
9 | templateUrl: './feedback-history-view.component.html',
10 | styleUrls: ['./feedback-history-view.component.scss']
11 | })
12 | export class FeedbackHistoryViewComponent implements OnInit {
13 |
14 | data: FeedbackHistoryPayload;
15 | readonly description = [`Find below a history of their feedback summaries. Colleagues were given the opportunity to provide feedback. \
16 | You or a previous line manager then reviewed the feedback and provided the summaries presented below.`,
17 | `Managers can view historical feedback about their Direct Reports spanning the last three feedback cycles. \
18 | Staff can see their full history of feedback.`];
19 |
20 | constructor(public api: ApiService, private route: ActivatedRoute) { }
21 |
22 | title(): string {
23 | if (this.data) {
24 | return `Feedback about ${this.data.feedback.displayName}`;
25 | }
26 | }
27 |
28 | ngOnInit() {
29 | this.route.params.pipe(
30 | map((p => p.username))
31 | ).subscribe((username: String) => {
32 | this.api.getFeedbackHistory(username).subscribe((data: FeedbackHistoryPayload) => {
33 | this.data = data;
34 | });
35 | });
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/give-feedback/give-feedback.component.html:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/give-feedback/give-feedback.component.scss:
--------------------------------------------------------------------------------
1 | #feedback-form-footer {
2 | color: lightslategray;
3 | font-size: smaller;
4 | padding-top: 1em;
5 | }
6 |
7 | #feedback-form-info {
8 | border-top: lightgray 1px solid;
9 | border-bottom: lightgray 1px solid;
10 | padding-top: 0.5em;
11 | padding-bottom: 0.5em;
12 | }
13 |
14 | #feedback-form-title {
15 | padding-bottom: 0.5em;
16 | }
--------------------------------------------------------------------------------
/frontend/src/app/components/views/give-feedback/give-feedback.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit, HostListener, ViewChild } from '@angular/core';
2 | import { NgForm } from '@angular/forms';
3 | import { ActivatedRoute, Router } from '@angular/router';
4 | import { Observable, Subscribable } from 'rxjs';
5 | import { map } from 'rxjs/operators';
6 |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
8 | import { ApiService, GiveFeedbackPayload, FeedbackFormItem, FeedbackForm } from '../../../services/api.service';
9 | import { FeedbackFormComponent, FormState } from '../../widgets';
10 | import { ComponentCanDeactivate } from '../../../guards/pending-changes-guard.service';
11 |
12 | @Component({
13 | selector: 'app-give-feedback',
14 | templateUrl: './give-feedback.component.html',
15 | styleUrls: ['./give-feedback.component.scss']
16 | })
17 | export class GiveFeedbackComponent implements ComponentCanDeactivate, OnInit {
18 |
19 | public formStates = FormState;
20 | form: FeedbackForm;
21 | username: String;
22 | formState: FormState = FormState.Editing;
23 | successButtonLink = '/feedback';
24 | @ViewChild('formComponent', { static: true }) formComponent: FeedbackFormComponent;
25 |
26 | readonly formInfo = `Thank you for taking the time to provide feedback on one of your colleagues.
27 | You will be asked to answer some simple questions:
28 |
29 | You should base your responses on your actual experience of working with this colleague.
30 | Feedback is most useful when you share specific examples, of observed behaviours
31 | and describe the impact this behaviour had.
32 | Feedback should motivate, improve performance and support continuous learning.
33 | Please consider this when providing your input.
34 | `;
35 |
36 | readonly successTitle = 'Feedback successfully saved!';
37 | readonly successButton = 'Give feedback';
38 |
39 |
40 | constructor(private api: ApiService, private route: ActivatedRoute, public modal: NgbModal) { }
41 |
42 | ngOnInit() {
43 | this.fetchData();
44 | }
45 |
46 | @HostListener('window:beforeunload')
47 | canDeactivate(): boolean {
48 | if (this.formComponent.templateForm) {
49 | return !this.formComponent.templateForm.dirty;
50 | } else {
51 | return true;
52 | }
53 | }
54 |
55 | successInfo(): string {
56 | if (this.form) {
57 | return `Thank you for taking the time to provide feedback on one of your colleagues.
58 | You can continue to give feedback until ${this.form.endDate}.`;
59 | }
60 | }
61 |
62 | saveCaption(): string {
63 | if (this.form) {
64 | return `Please kindly double check your feedback before proceeding: once you hit the Save button,
65 | your feedback will be saved and available for editing until the end of the "Give feedback" period at ${this.form.endDate}.
66 | After this time, it will automatically be submitted and released to your colleague's manager.` ;
67 | }
68 | }
69 |
70 | fetchData() {
71 | this.route.params.pipe(map(p => p.username)).subscribe((username: String) => {
72 | this.username = username;
73 | this.api.getFeedbackForm(username).subscribe((data: GiveFeedbackPayload) => {
74 | this.form = data.form;
75 | });
76 | });
77 | }
78 |
79 | onSubmit(form: NgForm) {
80 | this.formState = FormState.Submitting;
81 | this.api.putFeedbackForm(this.username, this.form.items).subscribe((isSuccess: any) => {
82 | this.formState = FormState.Success;
83 | },
84 | err => {
85 | this.formState = FormState.Editing;
86 | console.log('Request for PUT failed');
87 | throw err;
88 | });
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/index.ts:
--------------------------------------------------------------------------------
1 | export * from './give-feedback/give-feedback.component';
2 | export * from './feedback-about-me/feedback-about-me.component';
3 | export * from './nominees-list/nominees-list.component';
4 | export * from './login/login.component';
5 | export * from './self-nominate/self-nominate.component';
6 | export * from './stats/stats.component';
7 | export * from './summarise-feedback/summarise-feedback.component';
8 | export * from './talent-manager-panel/talent-manager-panel.component';
9 | export * from './authenticated/authenticated.component';
10 | export * from './feedback-history-view/feedback-history-view.component';
11 | export * from './external-invite/external-invite.component';
12 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/login/login.component.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
Sign In
13 |
14 |
15 | Browser unable to contact the backend service. Please contact your support for this application.
16 |
17 |
18 |
19 |
20 | {{ this.message }}
21 |
22 |
23 |
24 |
25 | {{ this.warnMessage }}
26 |
27 |
28 |
47 |
48 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/login/login.component.scss:
--------------------------------------------------------------------------------
1 | @import '~styles.scss';
2 |
3 | .container {
4 | padding-top: 1em;
5 | }
6 |
7 | #login-button {
8 | margin-top: 1em;
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/login/login.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 | import { FormsModule } from '@angular/forms';
3 | import { RouterTestingModule } from '@angular/router/testing';
4 |
5 | import { ApiService } from '../../../services/api.service';
6 | import { HttpClientModule } from '@angular/common/http';
7 | import { LoginComponent } from './login.component';
8 |
9 | describe('LoginComponent', () => {
10 | let component: LoginComponent;
11 | let fixture: ComponentFixture;
12 |
13 | beforeEach(async(() => {
14 | TestBed.configureTestingModule({
15 | imports: [ FormsModule, HttpClientModule, RouterTestingModule ],
16 | declarations: [ LoginComponent ],
17 | providers: [ ApiService ]
18 | })
19 | .compileComponents();
20 | }));
21 |
22 | beforeEach(() => {
23 | fixture = TestBed.createComponent(LoginComponent);
24 | component = fixture.componentInstance;
25 | fixture.detectChanges();
26 | });
27 |
28 | it('should create', () => {
29 | expect(component).toBeTruthy();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/login/login.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit, OnChanges } from '@angular/core';
2 | import { HttpErrorResponse } from '@angular/common/http';
3 | import { NgForm } from '@angular/forms';
4 | import { Router, ActivatedRoute } from '@angular/router';
5 |
6 | import { ApiService, MetadataPayload, Metadata } from '../../../services/api.service';
7 | import { CookieService } from '../../../services/cookie.service';
8 |
9 |
10 | // avoiding having password persisted in javascript
11 | class LoginForm {
12 | username?: string;
13 | rememberMe?: boolean;
14 | }
15 |
16 | @Component({
17 | templateUrl: './login.component.html',
18 | styleUrls: ['./login.component.scss'],
19 | selector: 'app-login'
20 | })
21 | export class LoginComponent implements OnInit, OnChanges {
22 |
23 | public message: string;
24 | public warnMessage: string;
25 | public metadata: Metadata;
26 | form: LoginForm = { rememberMe: false };
27 | redirectUrl = '/feedback';
28 | submitting = false;
29 | isDisconnected = false;
30 |
31 | constructor(public api: ApiService, private cookie: CookieService, private router: Router, private route: ActivatedRoute) {}
32 |
33 | ngOnInit() {
34 | const username = this.cookie.getCookie('username');
35 | if (username) {
36 | this.form.rememberMe = true;
37 | this.form.username = username;
38 | }
39 | if (this.route.snapshot.queryParams.logoutSuccess) {
40 | this.message = 'You have successfully logged out.';
41 | }
42 | if (this.route.snapshot.queryParams.from) {
43 | this.redirectUrl = this.route.snapshot.queryParams.from;
44 | }
45 | this.api.getMetadata().subscribe(
46 | (result: MetadataPayload) => {
47 | this.metadata = result.metadata;
48 | if (result.metadata.passwordlessAccess) {
49 | this.warnMessage = 'UNSAFE PASSWORDLESS ACCESS IS ENABLED ON WEB SERVER. THIS SHOULD NOT BE ON IN PRODUCTION!';
50 | }
51 | },
52 | () => {
53 | this.isDisconnected = true;
54 | }
55 | );
56 | }
57 |
58 | ngOnChanges() {
59 | if (this.form.rememberMe) {
60 | this.cookie.setCookie('username', this.form.username);
61 | }
62 | }
63 |
64 | onRememberMeToggle(value: boolean) {
65 | this.form.rememberMe = value;
66 | if (value) {
67 | this.cookie.setCookie('username', this.form.username);
68 | } else {
69 | this.cookie.deleteCookie('username');
70 | }
71 | }
72 |
73 | private setErrorMessage(error: HttpErrorResponse) {
74 | switch (error.status) {
75 | case 401:
76 | this.message = `Incorrect username and/or password. Please email ${this.metadata.supportEmail} if you have anymore issues`;
77 | break;
78 | case 404:
79 | this.message = `User with Windows username "${this.form.username}" not found. If you think this
80 | should not be the case, please email ${this.metadata.supportEmail} to synchronise User models with LDAP.`;
81 | break;
82 | case 500:
83 | this.message = `Backend service unable to serve the request. Please email ${this.metadata.supportEmail}.`;
84 | break;
85 | default:
86 | this.message = `Unable to login. Please email ${this.metadata.supportEmail} if you have any more issues.`;
87 | }
88 | }
89 |
90 | onSubmit(loginForm: NgForm) {
91 | this.submitting = true;
92 | this.message = null;
93 | this.api.login(loginForm.value.username, loginForm.value.password).subscribe(
94 | _ => {
95 | this.router.navigate([this.redirectUrl]);
96 | if (this.form.rememberMe) {
97 | this.cookie.setCookie('username', this.form.username);
98 | }
99 | this.submitting = false;
100 | },
101 | err => {
102 | this.setErrorMessage(err);
103 | this.submitting = false;
104 | }
105 | );
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/nominees-list/nominees-list.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Colleagues that have requested feedback
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | No nominees found!
20 |
21 |
22 | {{employee.displayName}}
23 | {{employee.position}}
24 | {{employee.department}}
25 | {{employee.managerDisplayName}}
26 |
27 |
28 |
29 | Give feedback
30 |
31 |
32 |
33 |
34 | View and edit
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
{{ data.heading }}
44 |
{{ data.body }}
45 |
46 | {{ data.buttonText }}
47 |
48 |
49 |
50 |
51 |
52 | Loading Data...
53 |
54 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/nominees-list/nominees-list.component.scss:
--------------------------------------------------------------------------------
1 | .nominee-header {
2 | margin: auto;
3 | }
4 | .nominee-action-header {
5 | text-align: center;
6 | margin: auto;
7 | }
8 |
9 | #nominee-quick-search-input {
10 | margin-bottom: 1em;
11 | }
--------------------------------------------------------------------------------
/frontend/src/app/components/views/nominees-list/nominees-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit } from '@angular/core';
2 | import { ApiService, NomineePayload, MessageTemplatePayload } from '../../../services/api.service';
3 | import { Observable } from 'rxjs';
4 |
5 | @Component({
6 | selector: 'app-nominees-list',
7 | templateUrl: './nominees-list.component.html',
8 | styleUrls: ['./nominees-list.component.scss']
9 | })
10 | export class NomineesListComponent implements OnInit {
11 | constructor(public api: ApiService) {}
12 |
13 | data: Observable<{} | NomineePayload | MessageTemplatePayload>;
14 |
15 | ngOnInit() {
16 | this.data = this.api.getNominees();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/self-nominate/self-nominate.component.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ this.data.heading }}
3 |
{{ this.data.body }}
4 |
5 | {{ this.data.buttonText }}
6 |
7 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/self-nominate/self-nominate.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 |
4 | import { ApiService } from '../../../services/api.service';
5 | import { HttpClientModule } from '@angular/common/http';
6 | import { SelfNominateComponent } from './self-nominate.component';
7 |
8 | describe('SelfNominateComponent', () => {
9 | let component: SelfNominateComponent;
10 | let fixture: ComponentFixture;
11 |
12 | beforeEach(async(() => {
13 | TestBed.configureTestingModule({
14 | imports: [ HttpClientModule, RouterTestingModule ],
15 | declarations: [ SelfNominateComponent ],
16 | providers: [ ApiService ]
17 | })
18 | .compileComponents();
19 | }));
20 |
21 | beforeEach(() => {
22 | fixture = TestBed.createComponent(SelfNominateComponent);
23 | component = fixture.componentInstance;
24 | fixture.detectChanges();
25 | });
26 |
27 | it('should create', () => {
28 | expect(component).toBeTruthy();
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/self-nominate/self-nominate.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit } from '@angular/core';
2 | import { Router } from '@angular/router';
3 |
4 | import { ApiService } from '../../../services/api.service';
5 |
6 | @Component({
7 | templateUrl: './self-nominate.component.html',
8 | selector: 'app-self-nominate'
9 | })
10 | export class SelfNominateComponent implements OnInit {
11 | isLoaded = false;
12 | data = null;
13 | endpoint = '/self-nominate';
14 |
15 | constructor(private api: ApiService, private router: Router) {}
16 |
17 | ngOnInit() {
18 | this.fetchTemplate();
19 | }
20 |
21 | fetchTemplate() {
22 | this.api.fetchTemplate(this.endpoint).subscribe(
23 | (data) => {
24 | this.data = data;
25 | this.isLoaded = true;
26 | }
27 | );
28 | }
29 |
30 | onClick() {
31 | if (this.data.canNominate) {
32 | this.api.selfNominate().subscribe(
33 | (data) => {
34 | this.data = data;
35 | this.isLoaded = true;
36 | }
37 | );
38 | } else {
39 | this.router.navigate([this.data.buttonLink]);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/stats/stats.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Feedback History
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{ row[0].displayName }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 0" *ngIf="i % 2 == 0;">
52 | {{ cell }}
53 |
54 | = 0" *ngIf="i % 2 == 1;">
55 | {{ cell }}
56 |
57 |
58 |
59 |
62 | {{ row[row.length - 1].buttonText }}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | Legend:
74 |
75 |
76 |
77 |
78 | 0 Did not give any feedback
79 |
80 |
81 | 5 Has given feedback to 5 colleagues
82 |
83 |
84 | -1 Did not request feedback
85 |
86 |
87 | 0 Requested feedback, but did not receive any feedback
88 |
89 |
90 | 3 Requested feedback, received feedback from 3 colleagues
91 |
92 |
93 |
94 |
95 |
96 | Loading Data...
--------------------------------------------------------------------------------
/frontend/src/app/components/views/stats/stats.component.scss:
--------------------------------------------------------------------------------
1 | @import '~styles.scss';
2 |
3 | $fixedColumnWidth: 5em;
4 |
5 | .stats-header {
6 | text-align: center;
7 | margin: auto;
8 | }
9 |
10 | #name-cell-padding {
11 | padding: 1.5em;
12 | background-color: white;
13 | border-color: white;
14 | }
15 |
16 | .name-cell {
17 | white-space: nowrap;
18 | }
19 |
20 | .stats-cell {
21 | text-align: center;
22 | }
23 |
24 | .stats-counter {
25 | border-radius: 50%;
26 | background: white;
27 | color: white;
28 | margin: auto;
29 | display: inline-block;
30 | text-align: center;
31 | vertical-align: middle;
32 | width: 2em;
33 | height: 2em;
34 | }
35 |
36 | .received {
37 | border: 2px solid green;
38 | }
39 | .nominated {
40 | background: green;
41 | }
42 |
43 | .sent {
44 | border: 2px solid grey;
45 | }
46 | .contributed {
47 | background: grey;
48 | }
49 |
50 | .stats-legend {
51 | display: flex;
52 | padding-top: 1em;
53 | font-size: smaller;
54 | }
55 |
56 | .stats-legend-title {
57 | padding-top: 0.5em;
58 | padding-right: 1em;
59 | padding-bottom: 1em;
60 | padding-left: 0.5em;
61 | }
62 |
63 | .stats-legend-content {
64 | ul {
65 | padding-left: 0em;
66 | list-style-type: none;
67 | li {
68 | padding-top: 0.5em;
69 | padding-bottom: 0.5em;
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/frontend/src/app/components/views/stats/stats.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { StatsComponent } from './stats.component';
4 |
5 | describe('StatsComponent', () => {
6 | let component: StatsComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ StatsComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(StatsComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/stats/stats.component.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
2 | import { ActivatedRoute } from '@angular/router';
3 | import { ApiService, StatsPayload } from '../../../services/api.service';
4 | import { Observable } from 'rxjs';
5 |
6 | @Component({
7 | selector: 'app-stats',
8 | templateUrl: './stats.component.html',
9 | styleUrls: ['./stats.component.scss']
10 | })
11 | export class StatsComponent implements OnInit, AfterViewChecked {
12 |
13 | @ViewChild('statsTable') private statsTable: ElementRef;
14 | @ViewChild('nameTable') private nameTable: ElementRef;
15 | data: Observable;
16 |
17 | constructor(public api: ApiService, private route: ActivatedRoute) { }
18 |
19 | ngOnInit() {
20 | this.route.data.subscribe((routeData) => {
21 | if (!routeData.isCompanyWide) {
22 | this.data = this.api.getTeamStats();
23 | } else {
24 | this.data = this.api.getCompanyStats();
25 | }
26 | this.scrollToEnd();
27 | });
28 | }
29 |
30 | ngAfterViewChecked() {
31 | this.scrollToEnd();
32 | this.syncTableCellHeights();
33 | }
34 |
35 | syncTableCellHeights() {
36 | // because we use two separate tables to be able to have a responsive yet fixed
37 | // first column
38 | try {
39 | const cell = this.statsTable.nativeElement.querySelector('.stats-cell');
40 | this.nameTable.nativeElement.querySelectorAll('td').forEach(element => {
41 | element.height = `${cell.offsetHeight}px` ;
42 | });
43 | } catch (err) {}
44 | }
45 |
46 | scrollToEnd() {
47 | try {
48 | this.statsTable.nativeElement.scrollLeft = window.screen.width;
49 | } catch (err) {}
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/summarise-feedback/summarise-feedback.component.html:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/summarise-feedback/summarise-feedback.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/frontend/src/app/components/views/summarise-feedback/summarise-feedback.component.scss
--------------------------------------------------------------------------------
/frontend/src/app/components/views/summarise-feedback/summarise-feedback.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { SummariseFeedbackComponent } from './summarise-feedback.component';
4 |
5 | describe('SummariseFeedbackComponent', () => {
6 | let component: SummariseFeedbackComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ SummariseFeedbackComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(SummariseFeedbackComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/summarise-feedback/summarise-feedback.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewChild, HostListener } from '@angular/core';
2 | import { NgForm } from '@angular/forms';
3 | import { ActivatedRoute, Router } from '@angular/router';
4 | import { ApiService, SummaryFeedbackPayload, FeedbackForm } from '../../../services/api.service';
5 | import { Observable } from 'rxjs';
6 | import { map } from 'rxjs/operators';
7 | import { FormState } from '../../../components/widgets/feedback-form/feedback-form.component';
8 | import { ComponentCanDeactivate } from '../../../guards/pending-changes-guard.service';
9 | import { FeedbackFormComponent } from '../../widgets';
10 |
11 | @Component({
12 | selector: 'app-summarise-feedback',
13 | templateUrl: './summarise-feedback.component.html',
14 | styleUrls: ['./summarise-feedback.component.scss']
15 | })
16 | export class SummariseFeedbackComponent implements OnInit {
17 |
18 | public formStates = FormState;
19 | form: FeedbackForm;
20 | username: String;
21 | formState: FormState = FormState.Editing;
22 | successButtonLink = '/team-feedback';
23 | @ViewChild('formComponent', { static: true }) formComponent: FeedbackFormComponent;
24 |
25 | readonly formInfo = `For each question, please review, edit & summarize the feedback that has been given:
26 |
27 | The raw contributions are in the first box which is greyed out
28 | Please enter your summary into the second box, which has been prefilled with the raw contributions.
29 | `;
30 |
31 | readonly successTitle = 'Summary successfully saved!';
32 | readonly successButton = 'Back to team stats page';
33 |
34 | constructor(public api: ApiService, private route: ActivatedRoute) { }
35 |
36 | @HostListener('window:beforeunload')
37 | canDeactivate(): boolean {
38 | if (this.formComponent.templateForm) {
39 | return !this.formComponent.templateForm.dirty;
40 | } else {
41 | return true;
42 | }
43 | }
44 |
45 | ngOnInit() {
46 | this.fetchData();
47 | }
48 |
49 | successInfo(): string {
50 | if (this.form) {
51 | return `Thank you for taking the time to summarise feedback on ${this.form.employee.displayName}.
52 | You should complete all feedback summaries by ${this.form.endDate}. Failure to meet this deadline
53 | will result in your remaining direct reports receiving no feedback from their colleagues.`;
54 | }
55 | }
56 |
57 | saveCaption(): string {
58 | if (this.form) {
59 | return `Please kindly double check your summary before proceeding: once you hit the Save button,
60 | your feedback will be saved and available for editing until the end of the "Review feedback" period at ${this.form.endDate}.
61 | After this time, it will be automatically released to your direct report(s).` ;
62 | }
63 | }
64 |
65 | fetchData() {
66 | this.route.params.pipe(map(p => p.username)).subscribe((username: String) => {
67 | this.username = username;
68 | this.api.getSummaryFeedback(username).subscribe((data: SummaryFeedbackPayload) => {
69 | this.form = data.summary;
70 | });
71 | });
72 | }
73 |
74 | onSubmit(form: NgForm) {
75 | this.formState = FormState.Submitting;
76 | this.api.putSummaryFeedback(this.username, this.form.items).subscribe((isSuccess: any) => {
77 | this.formState = FormState.Success;
78 | },
79 | err => {
80 | this.formState = FormState.Editing;
81 | console.log('Request to PUT form failed');
82 | throw err;
83 | });
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/talent-manager-panel/talent-manager-panel.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
Send emails
11 |
Initial emails are automatically sent by the system but you can use this page to send the reminders.
12 |
13 | {{ info.text }}
14 |
15 |
If there is an issue with the system and you would like to send out the initial emails again, you can use these to send them out manually.
16 |
17 | {{ info.text }}
18 |
19 |
20 |
Manage users
21 |
Download current user population
22 |
23 |
24 | There are currently {{ data?.userCount }} users stored in the database.
25 |
26 | Download current users
27 |
28 |
Generate population CSV template
29 |
{{ data?.generatePopulationMsg }}
30 |
36 |
37 |
Upload new population CSV
38 |
{{ data?.uploadNewPopulationMsg }}
39 |
45 |
46 |
--------------------------------------------------------------------------------
/frontend/src/app/components/views/talent-manager-panel/talent-manager-panel.component.scss:
--------------------------------------------------------------------------------
1 | .email-btn {
2 | margin-bottom: 0.5em;
3 | }
--------------------------------------------------------------------------------
/frontend/src/app/components/views/talent-manager-panel/talent-manager-panel.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { TalentManagerPanelComponent } from './talent-manager-panel.component';
4 |
5 | describe('TalentManagerPanelComponent', () => {
6 | let component: TalentManagerPanelComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ TalentManagerPanelComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(TalentManagerPanelComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/feedback-form/feedback-form.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Feedback on {{ form.employee.displayName }} for {{ form.periodName }}
6 |
7 |
8 |
9 |
26 |
29 |
30 |
31 |
36 |
37 |
38 |
39 | Loading Data...
40 |
41 |
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/feedback-form/feedback-form.component.scss:
--------------------------------------------------------------------------------
1 | #feedback-form-footer {
2 | color: lightslategray;
3 | font-size: smaller;
4 | padding-top: 1em;
5 | }
6 |
7 | #feedback-form-info {
8 | border-top: lightgray 1px solid;
9 | border-bottom: lightgray 1px solid;
10 | padding-top: 0.5em;
11 | padding-bottom: 0.5em;
12 | }
13 |
14 | #feedback-form-title {
15 | padding-bottom: 0.5em;
16 | }
17 |
18 | #feedback-form-form {
19 | padding-top: 0.5em;
20 | }
21 |
22 | .readonly-answer {
23 | white-space: pre-line;
24 | }
25 |
26 | label {
27 | font-weight: bold;
28 | }
29 |
30 | @media print {
31 | .caption, .raw-answer, .summary-label {
32 | display: none;
33 | }
34 | }
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/feedback-form/feedback-form.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { FeedbackFormComponent } from './feedback-form.component';
4 |
5 | describe('FeedbackFormComponent', () => {
6 | let component: FeedbackFormComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ FeedbackFormComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(FeedbackFormComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/feedback-form/feedback-form.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
2 | import { NgForm } from '@angular/forms';
3 | import { SummaryFeedbackPayload, FeedbackForm } from '../../../services/api.service';
4 |
5 | export enum FormState {
6 | Editing,
7 | Submitting,
8 | Success,
9 | }
10 |
11 | @Component({
12 | selector: 'app-feedback-form',
13 | templateUrl: './feedback-form.component.html',
14 | styleUrls: ['./feedback-form.component.scss']
15 | })
16 | export class FeedbackFormComponent {
17 |
18 | public formStates = FormState;
19 | @Input() form: FeedbackForm;
20 | @Input() public formState: FormState;
21 | @Input() public successButtonLink: string;
22 | @Input() public formInfo: string;
23 | @Input() public saveCaption: string;
24 | @Input() public successTitle: string;
25 | @Input() public successInfo: string;
26 | @Input() public successButton: string;
27 |
28 | @Output() submitRequest = new EventEmitter();
29 | @ViewChild('f') templateForm: NgForm;
30 |
31 | username: String;
32 |
33 | onSubmit(form: NgForm) {
34 | this.submitRequest.emit(form);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/feedback-history/feedback-history.component.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ title }}
3 |
{{ d }}
4 |
5 |
9 |
10 |
11 |
12 | {{ number + 1}}. {{row.question}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Loading Data...
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/feedback-history/feedback-history.component.scss:
--------------------------------------------------------------------------------
1 | .form-control[readonly] {
2 | background: white;
3 | }
4 |
5 | .history-info {
6 | white-space: pre-line;
7 | }
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/feedback-history/feedback-history.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { FeedbackHistoryComponent } from './feedback-history.component';
4 |
5 | describe('FeedbackHistoryComponent', () => {
6 | let component: FeedbackHistoryComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ FeedbackHistoryComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(FeedbackHistoryComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/feedback-history/feedback-history.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, Input } from '@angular/core';
2 | import { FeedbackHistoryPayload } from '../../../services/api.service';
3 |
4 | @Component({
5 | selector: 'app-feedback-history',
6 | templateUrl: './feedback-history.component.html',
7 | styleUrls: ['./feedback-history.component.scss']
8 | })
9 | export class FeedbackHistoryComponent implements OnInit {
10 |
11 | @Input() public data: FeedbackHistoryPayload;
12 | @Input() public title: string;
13 | @Input() public description: string;
14 |
15 | constructor() { }
16 |
17 | ngOnInit() {
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/index.ts:
--------------------------------------------------------------------------------
1 | export * from './feedback-form/feedback-form.component';
2 | export * from './modal/modal.component';
3 |
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/modal/modal.component.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ dialog }}
3 |
{{ metadata }}
4 |
5 |
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/modal/modal.component.scss:
--------------------------------------------------------------------------------
1 | .metadata {
2 | font-size: 1em;
3 | font-family: monospace;
4 | }
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/modal/modal.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ModalComponent } from './modal.component';
4 |
5 | describe('ModalComponent', () => {
6 | let component: ModalComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ ModalComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(ModalComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/frontend/src/app/components/widgets/modal/modal.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output } from '@angular/core';
2 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
3 |
4 | @Component({
5 | selector: 'app-modal',
6 | templateUrl: './modal.component.html',
7 | styleUrls: ['./modal.component.scss']
8 | })
9 | export class ModalComponent {
10 |
11 | @Input() public confirmText: string;
12 | @Input() public cancelText: string;
13 | @Input() public dialog: string;
14 | @Input() public metadata: string;
15 | constructor(public activeModal: NgbActiveModal) { }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/frontend/src/app/guards/anon-guard.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 | import { HttpClientModule } from '@angular/common/http';
3 | import { RouterTestingModule } from '@angular/router/testing';
4 | import { FormsModule } from '@angular/forms';
5 |
6 | import { AppRoutingModule } from '../app-routing.module';
7 | import { AnonGuardService } from './anon-guard.service';
8 | import { ApiService } from '../services/api.service';
9 | import {
10 | GiveFeedbackComponent,
11 | NomineesListComponent,
12 | LoginComponent,
13 | SelfNominateComponent
14 | } from '../components';
15 |
16 | describe('AnonGuardService', () => {
17 | beforeEach(() => {
18 | TestBed.configureTestingModule({
19 | declarations: [
20 | LoginComponent,
21 | NomineesListComponent,
22 | GiveFeedbackComponent,
23 | SelfNominateComponent
24 | ],
25 | imports: [
26 | FormsModule,
27 | HttpClientModule,
28 | RouterTestingModule
29 | ],
30 | providers: [
31 | AnonGuardService,
32 | ApiService
33 | ]
34 | });
35 | });
36 |
37 | it('should be created', inject([AnonGuardService], (service: AnonGuardService) => {
38 | expect(service).toBeTruthy();
39 | }));
40 | });
41 |
--------------------------------------------------------------------------------
/frontend/src/app/guards/anon-guard.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Router, CanActivate } from '@angular/router';
3 | import { Observable, of } from 'rxjs';
4 | import { catchError, map } from 'rxjs/operators';
5 |
6 |
7 |
8 |
9 | import { ApiService } from '../services/api.service';
10 |
11 |
12 | @Injectable()
13 | export class AnonGuardService implements CanActivate {
14 |
15 | constructor(private api: ApiService, private router: Router) {}
16 |
17 | canActivate() {
18 | console.log('AnonGuard#canActivate called');
19 | return this.api.getUserData().
20 | pipe(
21 | map((e) => {
22 | if (e) {
23 | this.router.navigate(['/self-nominate']);
24 | return false;
25 | }
26 | }),
27 | catchError((err) => {
28 | return of(true);
29 | })
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/app/guards/auth-guard.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 |
4 | import { AuthGuardService } from './auth-guard.service';
5 | import { ApiService } from '../services/api.service';
6 | import { HttpClientModule } from '@angular/common/http';
7 |
8 | describe('AuthGuardService', () => {
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | imports: [ HttpClientModule, RouterTestingModule ],
12 | providers: [AuthGuardService, ApiService]
13 | });
14 | });
15 |
16 | it('should be created', inject([AuthGuardService], (service: AuthGuardService) => {
17 | expect(service).toBeTruthy();
18 | }));
19 | });
20 |
--------------------------------------------------------------------------------
/frontend/src/app/guards/auth-guard.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router';
3 | import { Observable, of } from 'rxjs';
4 | import { catchError, map } from 'rxjs/operators';
5 |
6 |
7 |
8 |
9 | import { ApiService } from '../services/api.service';
10 |
11 |
12 | @Injectable()
13 | export class AuthGuardService implements CanActivate {
14 |
15 | constructor(private api: ApiService, private router: Router) {}
16 |
17 | canActivate(state: ActivatedRouteSnapshot) {
18 | const url = state.firstChild.url.join('/');
19 | console.log(`AuthGuard#canActivate called from ${url}`);
20 | return this.api.getUserData().
21 | pipe(
22 | map((e) => {
23 | if (e) { return true; }
24 | }),
25 | catchError((err) => {
26 | this.router.navigate(['/login'], {queryParams: {from: url}});
27 | return of(false);
28 | })
29 | );
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/frontend/src/app/guards/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auth-guard.service';
2 | export * from './anon-guard.service';
3 | export * from './pending-changes-guard.service';
4 |
--------------------------------------------------------------------------------
/frontend/src/app/guards/pending-changes-guard.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { PendingChangesGuardService } from './pending-changes-guard.service';
4 |
5 | describe('PendingChangesGuardService', () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [PendingChangesGuardService]
9 | });
10 | });
11 |
12 | it('should be created', inject([PendingChangesGuardService], (service: PendingChangesGuardService) => {
13 | expect(service).toBeTruthy();
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/frontend/src/app/guards/pending-changes-guard.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, TemplateRef, HostListener } from '@angular/core';
2 | import { Router, CanDeactivate } from '@angular/router';
3 | import { Observable } from 'rxjs';
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
5 | import { ModalComponent } from '../components/widgets';
6 |
7 | export interface ComponentCanDeactivate {
8 | canDeactivate: () => boolean | Observable ;
9 | modal: NgbModal;
10 | }
11 |
12 | @Injectable()
13 | export class PendingChangesGuardService implements CanDeactivate {
14 |
15 | constructor() { }
16 |
17 | canDeactivate(component: ComponentCanDeactivate): boolean | Promise {
18 | if (!component.canDeactivate()) {
19 | // NOTE: this warning message will only be shown when navigating elsewhere within your angular app;
20 | // when navigating away from your angular app, the browser will show a generic warning message
21 | // see http://stackoverflow.com/a/42207299/7307355`
22 | const c = component.modal.open(ModalComponent);
23 | c.componentInstance.confirmText = 'Leave';
24 | c.componentInstance.cancelText = 'Stay';
25 | c.componentInstance.dialog = 'You have unsaved changes. Are you sure you want to leave?';
26 | return c.result.then(() => {
27 | return true;
28 | }, () => {
29 | return false;
30 | });
31 | } else {
32 | return true;
33 | }
34 |
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/frontend/src/app/pipes/nominee-filter.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { NomineeFilterPipe } from './nominee-filter.pipe';
2 |
3 | describe('NomineeFilterPipe', () => {
4 | it('create an instance', () => {
5 | const pipe = new NomineeFilterPipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/frontend/src/app/pipes/nominee-filter.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import { NomineeItem } from '../services/api.service';
3 |
4 | @Pipe({
5 | name: 'nomineeFilter'
6 | })
7 | export class NomineeFilterPipe implements PipeTransform {
8 |
9 | transform(nominees: NomineeItem[], criteria: string): any {
10 | const lowerCriteria = criteria.toLowerCase();
11 | return nominees.filter((nominee: NomineeItem) => {
12 | if (criteria.length < 3) {
13 | return true;
14 | } else if (nominee.displayName.toLowerCase().indexOf(lowerCriteria) >= 0) {
15 | return true;
16 | } else if (nominee.position.toLowerCase().indexOf(lowerCriteria) >= 0) {
17 | return true;
18 | } else if (nominee.managerDisplayName.toLowerCase().indexOf(lowerCriteria) >= 0) {
19 | return true;
20 | } else if (nominee.department.toLowerCase().indexOf(lowerCriteria) >= 0) {
21 | return true;
22 | } else {
23 | return false;
24 | }
25 | });
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/frontend/src/app/services/cookie.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { CookieService } from './cookie.service';
4 |
5 | describe('CookieService', () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [CookieService]
9 | });
10 | });
11 |
12 | it('should be created', inject([CookieService], (service: CookieService) => {
13 | expect(service).toBeTruthy();
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/frontend/src/app/services/cookie.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | @Injectable()
4 | export class CookieService {
5 |
6 | constructor() { }
7 |
8 | getCookie(name: string) {
9 | const cookies: Array = document.cookie.split(';');
10 | const cookieName = `${name}=`;
11 | let c: string;
12 |
13 | for (const cookie of cookies) {
14 | c = cookie.replace(/^\s+/g, '');
15 | if (c.indexOf(cookieName) === 0) {
16 | return c.substring(cookieName.length, c.length);
17 | }
18 | }
19 | return '';
20 | }
21 |
22 | deleteCookie(name) {
23 | this.setCookie(name, '', -1);
24 | }
25 |
26 | // number = 60 * 60 * 24 * 28
27 | setCookie(name: string, value: string, maxAgeSeconds: number = 2419200, path: string = '') {
28 | let maxAge = '';
29 | if (maxAgeSeconds) {
30 | maxAge = `max-age=${maxAgeSeconds}`;
31 | }
32 | const cpath = path ? `; path=${path}` : '';
33 | document.cookie = `${name}=${value}; ${maxAge}${cpath}`;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/src/assets/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/frontend/src/assets/favicon-16x16.png
--------------------------------------------------------------------------------
/frontend/src/assets/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/frontend/src/assets/favicon-32x32.png
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/frontend/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/frontend/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Adaero
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/frontend/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.log(err));
13 |
--------------------------------------------------------------------------------
/frontend/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /***************************************************************************************************
2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
3 | */
4 | import '@angular/localize/init';
5 | /**
6 | * This file includes polyfills needed by Angular and is loaded before the app.
7 | * You can add your own extra polyfills to this file.
8 | *
9 | * This file is divided into 2 sections:
10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
12 | * file.
13 | *
14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
17 | *
18 | * Learn more in https://angular.io/guide/browser-support
19 | */
20 |
21 | /***************************************************************************************************
22 | * BROWSER POLYFILLS
23 | */
24 |
25 |
26 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
27 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
28 |
29 | /** IE10 and IE11 requires the following for the Reflect API. */
30 | // import 'core-js/es6/reflect';
31 |
32 |
33 | /** Evergreen browsers require these. **/
34 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
35 | import 'core-js/es7/reflect';
36 |
37 |
38 | /**
39 | * Required to support Web Animations `@angular/platform-browser/animations`.
40 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
41 | **/
42 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
43 |
44 |
45 |
46 | /***************************************************************************************************
47 | * Zone JS is required by Angular itself.
48 | */
49 | import 'zone.js/dist/zone'; // Included with Angular CLI.
50 |
51 |
52 |
53 | /***************************************************************************************************
54 | * APPLICATION IMPORTS
55 | */
56 |
57 | /**
58 | * Date, currency, decimal and percent pipes.
59 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
60 | */
61 | // import 'intl'; // Run `npm install --save intl`.
62 | /**
63 | * Need to import at least one locale-data with intl.
64 | */
65 | // import 'intl/locale-data/jsonp/en';
66 |
--------------------------------------------------------------------------------
/frontend/src/styles.scss:
--------------------------------------------------------------------------------
1 | $grey: #f2f2f2;
2 | $grey-blue: #656c7e;
3 |
4 | // TODO: Move bootstrap import back to angular-cli.json once angular-cli is fixed https://github.com/angular/angular-cli/issues/9020
5 | @import '../node_modules/bootstrap/scss/bootstrap.scss';
6 | @import url(https://fonts.googleapis.com/css?family=Open+Sans);
7 |
8 | body {
9 | font-family: 'Open Sans', sans-serif;
10 | }
11 | #navbar {
12 | background: $grey-blue;
13 | padding: 1rem;
14 | }
15 |
16 | .navbar-header {
17 | padding-left: 0.5em;
18 | padding-top: 0.25em;
19 | padding-bottom: 0.25em;
20 | display: flex;
21 | align-items: stretch;
22 | }
23 |
24 | .unit-name-box {
25 | border-left: white 2px solid;
26 | padding-left: 0.75em;
27 | margin-left: 1.5em;
28 | display: flex;
29 | flex-direction: column;
30 | align-items: center;
31 | justify-content: center;
32 | }
33 | .unit-name {
34 | color: white;
35 | width: 100px;
36 | padding-left: 0.5em;
37 | }
38 |
--------------------------------------------------------------------------------
/frontend/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/long-stack-trace-zone';
4 | import 'zone.js/dist/proxy.js';
5 | import 'zone.js/dist/sync-test';
6 | import 'zone.js/dist/jasmine-patch';
7 | import 'zone.js/dist/async-test';
8 | import 'zone.js/dist/fake-async-test';
9 | import { getTestBed } from '@angular/core/testing';
10 | import {
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting
13 | } from '@angular/platform-browser-dynamic/testing';
14 |
15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
16 | declare const __karma__: any;
17 | declare const require: any;
18 |
19 | // Prevent Karma from running prematurely.
20 | __karma__.loaded = function () {};
21 |
22 | // First, initialize the Angular testing environment.
23 | getTestBed().initTestEnvironment(
24 | BrowserDynamicTestingModule,
25 | platformBrowserDynamicTesting()
26 | );
27 | // Then we find all the tests.
28 | const context = require.context('./', true, /\.spec\.ts$/);
29 | // And load the modules.
30 | context.keys().map(context);
31 | // Finally, start Karma to run the tests.
32 | __karma__.start();
33 |
--------------------------------------------------------------------------------
/frontend/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "baseUrl": "./",
6 | "types": []
7 | },
8 | "files": [
9 | "main.ts",
10 | "polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "baseUrl": "./",
6 | "types": [
7 | "jasmine",
8 | "node"
9 | ]
10 | },
11 | "files": [
12 | "test.ts",
13 | "polyfills.ts"
14 | ],
15 | "include": [
16 | "**/*.spec.ts",
17 | "**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/frontend/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare var module: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "downlevelIteration": true,
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "moduleResolution": "node",
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "target": "es2015",
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "lib": [
16 | "es2017",
17 | "dom"
18 | ],
19 | "module": "esnext",
20 | "baseUrl": "./"
21 | }
22 | }
--------------------------------------------------------------------------------
/frontend/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "eofline": true,
15 | "forin": true,
16 | "import-blacklist": [
17 | true,
18 | "rxjs/Rx"
19 | ],
20 | "import-spacing": true,
21 | "indent": [
22 | true,
23 | "spaces"
24 | ],
25 | "interface-over-type-literal": true,
26 | "label-position": true,
27 | "max-line-length": [
28 | true,
29 | 140
30 | ],
31 | "member-access": false,
32 | "member-ordering": [
33 | true,
34 | {
35 | "order": [
36 | "static-field",
37 | "instance-field",
38 | "static-method",
39 | "instance-method"
40 | ]
41 | }
42 | ],
43 | "no-arg": true,
44 | "no-bitwise": true,
45 | "no-console": [
46 | true,
47 | "debug",
48 | "info",
49 | "time",
50 | "timeEnd",
51 | "trace"
52 | ],
53 | "no-construct": true,
54 | "no-debugger": true,
55 | "no-duplicate-super": true,
56 | "no-empty": false,
57 | "no-empty-interface": true,
58 | "no-eval": true,
59 | "no-inferrable-types": [
60 | true,
61 | "ignore-params"
62 | ],
63 | "no-misused-new": true,
64 | "no-non-null-assertion": true,
65 | "no-shadowed-variable": true,
66 | "no-string-literal": false,
67 | "no-string-throw": true,
68 | "no-switch-case-fall-through": true,
69 | "no-trailing-whitespace": true,
70 | "no-unnecessary-initializer": true,
71 | "no-unused-expression": true,
72 | "no-use-before-declare": true,
73 | "no-var-keyword": true,
74 | "object-literal-sort-keys": false,
75 | "one-line": [
76 | true,
77 | "check-open-brace",
78 | "check-catch",
79 | "check-else",
80 | "check-whitespace"
81 | ],
82 | "prefer-const": true,
83 | "quotemark": [
84 | true,
85 | "single"
86 | ],
87 | "radix": true,
88 | "semicolon": [
89 | true,
90 | "always"
91 | ],
92 | "triple-equals": [
93 | true,
94 | "allow-null-check"
95 | ],
96 | "typedef-whitespace": [
97 | true,
98 | {
99 | "call-signature": "nospace",
100 | "index-signature": "nospace",
101 | "parameter": "nospace",
102 | "property-declaration": "nospace",
103 | "variable-declaration": "nospace"
104 | }
105 | ],
106 | "typeof-compare": true,
107 | "unified-signatures": true,
108 | "variable-name": false,
109 | "whitespace": [
110 | true,
111 | "check-branch",
112 | "check-decl",
113 | "check-operator",
114 | "check-separator",
115 | "check-type"
116 | ],
117 | "directive-selector": [
118 | true,
119 | "attribute",
120 | "app",
121 | "camelCase"
122 | ],
123 | "component-selector": [
124 | true,
125 | "element",
126 | "app",
127 | "kebab-case"
128 | ],
129 | "no-inputs-metadata-property": true,
130 | "no-outputs-metadata-property": true,
131 | "no-host-metadata-property": true,
132 | "no-input-rename": true,
133 | "no-output-rename": true,
134 | "use-lifecycle-interface": true,
135 | "use-pipe-transform-interface": true,
136 | "component-class-suffix": true,
137 | "directive-class-suffix": true,
138 | "invoke-injectable": true
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/gunicorn_starter.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | gunicorn --paste /usr/src/app/adaero/example.ini -w 2 --threads 2 -b 0.0.0.0:8080
--------------------------------------------------------------------------------
/install-packages.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Bash "strict mode", to help catch problems and bugs in the shell
4 | # script. Every bash script you write should include this. See
5 | # http://redsymbol.net/articles/unofficial-bash-strict-mode/ for
6 | # details.
7 | set -euo pipefail
8 |
9 | # Tell apt-get we're never going to be able to give manual
10 | # feedback:
11 | export DEBIAN_FRONTEND=noninteractive
12 |
13 | # Update the package listing, so we know what package exist:
14 | apt-get update
15 |
16 | # Install security updates:
17 | apt-get -y upgrade
18 |
19 | # Install a new package, without unnecessary recommended packages:
20 | apt-get -y install --no-install-recommends \
21 | python3.6 curl ca-certificates \
22 | autoconf build-essential \
23 | libxml2-dev libxslt-dev python3-dev \
24 | libldap2-dev libsasl2-dev slapd ldap-utils
25 |
26 | # Delete cached files we don't need anymore:
27 | apt-get clean
28 | rm -rf /var/lib/apt/lists/*
29 |
--------------------------------------------------------------------------------
/population.dot:
--------------------------------------------------------------------------------
1 | strict digraph {
2 | {
3 | l [label="LDAP", shape=cylinder]
4 | generate_population_process [style=filled, fillcolor=grey, label="talent manager invokes /api/v1/generate-population.csv via web frontend"]
5 | raw_csv [label="Raw staff list CSV", shape=note]
6 | modify_csv_process [style=filled, fillcolor=grey, label="talent manager manually edits CSV"]
7 | processed_csv [label="Curated staff list CSV", shape=note]
8 | upload_csv_process [style=filled, fillcolor=grey, label="talent manager uploaded processed CSV using /api/v1/upload-new-population-csv via web frontend"]
9 | database [shape=cylinder, label="Databse with staff information fixed at point of time of CSV upload"]
10 | }
11 | l -> generate_population_process
12 | generate_population_process -> raw_csv
13 | raw_csv -> modify_csv_process
14 | modify_csv_process -> processed_csv
15 | processed_csv -> upload_csv_process
16 | upload_csv_process -> database
17 | }
--------------------------------------------------------------------------------
/population.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/population.png
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/setup.cfg
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Always prefer setuptools over distutils
2 | from setuptools import setup, find_packages
3 |
4 | from os import path
5 |
6 | here = path.abspath(path.dirname(__file__))
7 |
8 | description = 'A platform for managing peer-to-peer feedback within an organisation.'
9 |
10 | def readme():
11 | try:
12 | with open('README.md') as f:
13 | return f.read()
14 | except FileNotFoundError:
15 | return ""
16 |
17 | setup(
18 | name='adaero',
19 |
20 | version='1.0.0',
21 |
22 | description=description,
23 | long_description=readme(),
24 | long_description_content_type='text/markdown',
25 | # The project's main homepage.
26 | url='https://github.com/man-group/adaero',
27 | download_url='https://github.com/man-group/adaero/archive/v1.0.0.tar.gz',
28 |
29 | # Author details
30 | author='MAN Alpha Tech',
31 | author_email='ManAlphaTech@man.com',
32 |
33 | # Choose your license
34 | license='AGPL 3.0',
35 |
36 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
37 | classifiers=[
38 | 'Development Status :: 4 - Beta',
39 | 'Programming Language :: Python :: 3.7',
40 | 'Programming Language :: Python :: 3.6',
41 | 'Programming Language :: Python :: 2.7',
42 | ],
43 |
44 | # What does your project relate to?
45 | keywords='feedback',
46 |
47 | # You can just specify the packages manually here if your project is
48 | # simple. Or you can use find_packages().
49 | packages=find_packages(exclude=['contrib', 'docs', 'tests']),
50 |
51 | # Alternatively, if you want to distribute just a my_module.py, uncomment
52 | # this:
53 | # py_modules=["my_module"],
54 |
55 | # List run-time dependencies here. These will be installed by pip when
56 | # your project is installed. For an analysis of "install_requires" vs pip's
57 | # requirements files see:
58 | # https://packaging.python.org/en/latest/requirements.html
59 | install_requires=[
60 | 'pyramid',
61 | 'pyramid-beaker',
62 | 'waitress',
63 | 'python-ldap',
64 | 'pyramid_tm',
65 | 'SQLAlchemy',
66 | 'transaction',
67 | 'zope.sqlalchemy==1.1',
68 | 'pycrypto',
69 | 'rest_toolkit',
70 | 'python-dateutil',
71 | 'apscheduler',
72 | 'Paste',
73 | 'beautifulsoup4',
74 | 'lxml',
75 | 'pytz',
76 | 'alembic',
77 | 'unicodecsv',
78 | 'jinja2',
79 | 'click',
80 | 'pandas',
81 | 'gunicorn',
82 | 'psycopg2-binary'
83 | ],
84 |
85 | # List additional groups of dependencies here (e.g. development
86 | # dependencies). You can install these using the following syntax,
87 | # for example:
88 | # $ pip install -e .[dev,test]
89 | extras_require={
90 | 'test': [
91 | 'pytest',
92 | 'pytest-cov',
93 | 'mock',
94 | 'python-dateutil',
95 | 'WebTest',
96 | 'faker',
97 | 'click',
98 | 'freezegun',
99 | 'pytest-xdist',
100 | 'tox'
101 | ],
102 | 'oracle': [
103 | 'cx_Oracle'
104 | ],
105 | 'postgres': [
106 | 'psycopg2'
107 | ]
108 | },
109 |
110 | # To provide executable scripts, use entry points in preference to the
111 | # "scripts" keyword. Entry points provide cross-platform support and allow
112 | # pip to create the appropriate form of executable for the target platform.
113 | entry_points={
114 | 'console_scripts': [
115 | 'adaero=adaero.scripts.feedback_app:main',
116 | 'configure_db=adaero.scripts.configure_db:cli',
117 | ],
118 | 'paste.app_factory': [
119 | 'main = adaero:main'
120 | ]
121 | },
122 | )
123 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/tests/__init__.py
--------------------------------------------------------------------------------
/tests/integration/README.md:
--------------------------------------------------------------------------------
1 | # Integration Tests
2 |
3 | Integration tests for this package. They are comprehensive - input to tests that have their names prepended
4 | with `_web` are essentially HTTP requests against a running Pyramid server connected to the dev oracle database.
5 |
--------------------------------------------------------------------------------
/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/tests/integration/__init__.py
--------------------------------------------------------------------------------
/tests/integration/conftest.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 | import pytest
4 | import transaction
5 | from pyramid import testing
6 | from sqlalchemy.dialects.oracle import LONG
7 | from sqlalchemy.ext.compiler import compiles
8 | import sqlalchemy.orm.session
9 |
10 | from tests.settings import DEFAULT_TEST_SETTINGS
11 | from adaero.models import (
12 | FeedbackQuestion,
13 | FeedbackTemplateRow,
14 | FeedbackTemplate,
15 | Period,
16 | Nominee,
17 | FeedbackForm,
18 | FeedbackAnswer,
19 | User,
20 | ExternalInvite,
21 | )
22 |
23 | from .constants import TEST_UTCNOW
24 |
25 |
26 | def days_from_utcnow(days, offset=0):
27 | return TEST_UTCNOW + timedelta(days=days + offset)
28 |
29 |
30 | def pytest_addoption(parser):
31 | parser.addoption("--use-sqlite3", default=False, action="store_true")
32 |
33 |
34 | # backport from pytest-xdist 1.15.0
35 | @pytest.fixture(scope="session")
36 | def worker_id(request):
37 | if hasattr(request.config, "slaveinput"):
38 | return request.config.slaveinput["slaveid"]
39 | else:
40 | return "master"
41 |
42 |
43 | @compiles(LONG, "sqlite")
44 | def compile_oracle_long(element, compiler, **kw):
45 | """Handles Oracle LONG datatype as text in sqlite."""
46 | return compiler.visit_text(element, **kw)
47 |
48 |
49 | @pytest.yield_fixture(scope="session")
50 | def dbsession(request, worker_id):
51 | """
52 | Properly setup, yield and teardown an Oracle backed SQLAlchemy session
53 |
54 | Make sure this is in sync with func_scoped_dbsession
55 | """
56 | settings = DEFAULT_TEST_SETTINGS
57 | if request.config.getoption("--use-sqlite3"):
58 | settings["adaero.use_local_sqlite3"] = True
59 | settings["adaero.worker_id"] = worker_id
60 |
61 | config = testing.setUp(settings=settings)
62 | config.include("adaero.models")
63 | settings = config.get_settings()
64 |
65 | from adaero.models import get_engine, get_session_factory, get_tm_session
66 |
67 | engine = get_engine(settings)
68 | session_factory = get_session_factory(engine)
69 | session = get_tm_session(session_factory, transaction.manager)
70 |
71 | from adaero.models.all import Base, SEQUENCES
72 |
73 | Base.metadata.drop_all(engine)
74 |
75 | for seq in SEQUENCES:
76 | seq.create(engine)
77 | Base.metadata.create_all(engine)
78 |
79 | yield session
80 |
81 | testing.tearDown()
82 | transaction.abort()
83 | Base.metadata.drop_all(engine)
84 |
85 |
86 | @pytest.yield_fixture
87 | def func_scoped_dbsession(dbsession):
88 | yield dbsession
89 | drop_everything(dbsession)
90 |
91 |
92 | def drop_everything_but_users(dbsession_):
93 | with transaction.manager:
94 | dbsession_.query(FeedbackAnswer).delete()
95 | dbsession_.query(FeedbackForm).delete()
96 | dbsession_.query(FeedbackTemplateRow).delete()
97 | dbsession_.query(FeedbackQuestion).delete()
98 | dbsession_.query(Nominee).delete()
99 | dbsession_.query(ExternalInvite).delete()
100 | dbsession_.query(Period).delete()
101 | dbsession_.query(FeedbackTemplate).delete()
102 |
103 |
104 | def drop_everything(dbsession_):
105 | drop_everything_but_users(dbsession_)
106 | with transaction.manager:
107 | dbsession_.query(User).delete()
108 |
109 |
110 | def dummy_request(dbsession):
111 | return testing.DummyRequest(dbsession=dbsession, user={})
112 |
113 |
114 | def next_day_generator(start=datetime.now(), step_backwards=False):
115 | if step_backwards:
116 | offset = -timedelta(days=1)
117 | else:
118 | offset = +timedelta(days=1)
119 | dt = start
120 | while 1:
121 | yield dt
122 | dt = dt + offset
123 |
124 |
125 | def get_dbsession(context):
126 | # Uses `scoped_session` to fine to call as many times desired and
127 | # will use existing connection pool
128 | if isinstance(context, sqlalchemy.orm.session.Session):
129 | return context
130 | return context.app.registry["dbsession_factory"]()
131 |
--------------------------------------------------------------------------------
/tests/integration/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/tests/integration/models/__init__.py
--------------------------------------------------------------------------------
/tests/integration/models/test_feedback.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import transaction
3 |
4 | from adaero.models import User, FeedbackAnswer, FeedbackForm
5 | from adaero.models.all import CheckError
6 | from adaero.constants import ANSWER_CHAR_LIMIT
7 | from adaero.security import ldapauth
8 |
9 | from ..views.test_manager import add_test_data_for_stats
10 | from ..constants import (
11 | TEST_LDAP_FULL_DETAILS,
12 | TEST_EMPLOYEE_2_USERNAME,
13 | QUESTION_IDS_AND_TEMPLATES,
14 | TEST_PERIOD_ID,
15 | TEST_MANAGER_USERNAME,
16 | TEST_TALENT_MANAGER_USERNAME,
17 | )
18 | from ...settings import DEFAULT_TEST_SETTINGS
19 |
20 | from ..conftest import drop_everything
21 |
22 |
23 | @pytest.yield_fixture(scope="module")
24 | def stats_session(dbsession):
25 | ldapsource = ldapauth.build_ldapauth_from_settings(DEFAULT_TEST_SETTINGS)
26 | with transaction.manager:
27 | for user_details in TEST_LDAP_FULL_DETAILS.values():
28 | dbsession.add(User.create_from_ldap_details(ldapsource, user_details))
29 | add_test_data_for_stats(dbsession)
30 | yield dbsession
31 | drop_everything(dbsession)
32 |
33 |
34 | def test_unable_to_create_two_summaries_for_a_given_period(stats_session): # noqa: E501
35 | # above setup means there is an existing summary for
36 | # TEST_EMPLOYEE_2_USERNAME
37 | dbsession = stats_session
38 | with pytest.raises(CheckError):
39 | with transaction.manager:
40 | manager_form = FeedbackForm(
41 | to_username=TEST_EMPLOYEE_2_USERNAME,
42 | from_username=TEST_MANAGER_USERNAME,
43 | period_id=TEST_PERIOD_ID,
44 | is_summary=True,
45 | )
46 | answers = [
47 | FeedbackAnswer(
48 | question_id=QUESTION_IDS_AND_TEMPLATES[0][0], content="Better not"
49 | ),
50 | FeedbackAnswer(
51 | question_id=QUESTION_IDS_AND_TEMPLATES[1][0],
52 | content="override existing",
53 | ),
54 | FeedbackAnswer(
55 | question_id=QUESTION_IDS_AND_TEMPLATES[2][0], content="answers!!!"
56 | ),
57 | ]
58 | manager_form.answers = answers
59 | dbsession.add(manager_form)
60 |
61 |
62 | def test_able_to_insert_long_utf8_text(stats_session):
63 | dbsession = stats_session
64 | with transaction.manager:
65 | manager_form = FeedbackForm(
66 | to_username=TEST_MANAGER_USERNAME,
67 | from_username=TEST_TALENT_MANAGER_USERNAME,
68 | period_id=TEST_PERIOD_ID,
69 | is_summary=False,
70 | )
71 | answers = [
72 | FeedbackAnswer(
73 | question_id=QUESTION_IDS_AND_TEMPLATES[0][0],
74 | content="" * ANSWER_CHAR_LIMIT,
75 | )
76 | ]
77 | manager_form.answers = answers
78 | dbsession.add(manager_form)
79 |
--------------------------------------------------------------------------------
/tests/integration/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/tests/integration/views/__init__.py
--------------------------------------------------------------------------------
/tests/integration/views/company_stats.csv:
--------------------------------------------------------------------------------
1 | employee_id,last_name,first_name,email,business_unit,location,line_manager_name,line_manager_email,2014-Q1_contributed,2017-Q4_contributed,2014-Q1_received,2017-Q4_received
2 | 123456,Boggs,Barney,bboggs@example.com,Alpha,London,Charles Dalton,cdalton@example.com,2,0,-1,0
3 | 321321,Dalton,Charles,cdalton@example.com,Alpha,Hong Kong,Lady Lovelace,llovelace@example.com,0,0,-1,-1
4 | 878787,Dodson,Dominic,ddodson@example.com,Alpha,London,Charles Dalton,cdalton@example.com,0,0,-1,-1
5 | 999999,Lovelace,Lady,llovelace@example.com,Alpha,London,,,3,10,8,2
6 | 123123,Sholes,Șarah,ssholes@example.com,Alpha,London,Charles Dalton,cdalton@example.com,7,2,3,4
7 |
--------------------------------------------------------------------------------
/tests/integration/views/test_auth.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from adaero.security import (
4 | ANGULAR_2_XSRF_TOKEN_COOKIE_NAME,
5 | ANGULAR_2_XSRF_TOKEN_HEADER_NAME,
6 | )
7 | from ..constants import TEST_MANAGER_USERNAME, TEST_PASSWORD, TEST_LDAP_FULL_DETAILS
8 | from .conftest import successfully_login
9 |
10 |
11 | def test_login(ldap_mocked_app_with_users):
12 | """GIVEN I am unauthenticated
13 | WHEN I login with the correct credentials
14 | THEN I am returned an XSRF token as a cookie and my user details"""
15 |
16 | app = ldap_mocked_app_with_users
17 |
18 | response = app.post_json(
19 | "/api/v1/login",
20 | {"username": "hacker", "password": "ishouldnotwork"},
21 | expect_errors=True,
22 | )
23 |
24 | assert ANGULAR_2_XSRF_TOKEN_COOKIE_NAME not in app.cookies
25 | assert response.status_code == 401, response.json
26 |
27 | response = app.post_json(
28 | "/api/v1/login", {"username": TEST_MANAGER_USERNAME, "password": TEST_PASSWORD}
29 | )
30 |
31 | manager_details = TEST_LDAP_FULL_DETAILS[TEST_MANAGER_USERNAME]
32 | display_name = " ".join([manager_details["givenName"], manager_details["sn"]])
33 | title = TEST_LDAP_FULL_DETAILS[TEST_MANAGER_USERNAME]["title"]
34 | generated_principals = [
35 | "system.Everyone",
36 | "system.Authenticated",
37 | "cdalton",
38 | "role:employee",
39 | "role:manager",
40 | "direct_report:ssholes",
41 | "direct_report:bboggs",
42 | "direct_report:ddodson",
43 | ]
44 |
45 | assert True is response.json_body["success"]
46 | assert display_name == response.json_body["data"]["displayName"]
47 | assert title == response.json_body["data"]["title"]
48 | assert "Alpha" == response.json_body["data"]["businessUnit"]
49 | assert sorted(generated_principals) == sorted(
50 | response.json_body["data"]["principals"]
51 | )
52 | assert ANGULAR_2_XSRF_TOKEN_COOKIE_NAME in app.cookies
53 |
54 |
55 | def test_logout(ldap_mocked_app_with_users):
56 | app = ldap_mocked_app_with_users
57 | successfully_login(app, TEST_MANAGER_USERNAME)
58 | csrf_token = app.cookies[ANGULAR_2_XSRF_TOKEN_COOKIE_NAME]
59 | app.post("/api/v1/logout", headers={ANGULAR_2_XSRF_TOKEN_HEADER_NAME: csrf_token})
60 |
61 |
62 | def test_user_data(ldap_mocked_app_with_users):
63 | app = ldap_mocked_app_with_users
64 |
65 | failed_resp = app.get("/api/v1/user-data", expect_errors=True)
66 | assert 401 == failed_resp.status_code
67 |
68 | app.post_json(
69 | "/api/v1/login", {"username": TEST_MANAGER_USERNAME, "password": TEST_PASSWORD}
70 | )
71 |
72 | response = app.get("/api/v1/user-data")
73 | manager_details = TEST_LDAP_FULL_DETAILS[TEST_MANAGER_USERNAME]
74 | display_name = " ".join([manager_details["givenName"], manager_details["sn"]])
75 | title = TEST_LDAP_FULL_DETAILS[TEST_MANAGER_USERNAME]["title"]
76 | generated_principals = [
77 | "system.Everyone",
78 | "system.Authenticated",
79 | "cdalton",
80 | "role:employee",
81 | "role:manager",
82 | "direct_report:ssholes",
83 | "direct_report:bboggs",
84 | "direct_report:ddodson",
85 | ]
86 |
87 | assert True is response.json_body["success"]
88 | assert display_name == response.json_body["data"]["displayName"]
89 | assert title == response.json_body["data"]["title"]
90 | assert "Alpha" == response.json_body["data"]["businessUnit"]
91 | assert sorted(generated_principals) == sorted(
92 | response.json_body["data"]["principals"]
93 | )
94 | assert ANGULAR_2_XSRF_TOKEN_COOKIE_NAME in app.cookies
95 |
--------------------------------------------------------------------------------
/tests/integration/views/test_metadata.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 | import copy
3 | import os
4 |
5 | import pytest
6 | import webtest
7 |
8 | import adaero.constants
9 |
10 | from tests.settings import DEFAULT_TEST_SETTINGS
11 |
12 |
13 | def test_can_get_correct_metadata_from_config():
14 | app = webtest.TestApp(adaero.main({}, **DEFAULT_TEST_SETTINGS))
15 | response = app.get("/api/v1/metadata")
16 | assert not response.json_body["metadata"]["passwordlessAccess"]
17 | dev_settings = copy.copy(DEFAULT_TEST_SETTINGS)
18 | dev_settings[adaero.constants.ALLOW_PASSWORDLESS_ACCESS_KEY] = True
19 | app = webtest.TestApp(adaero.main({}, **dev_settings))
20 | response = app.get("/api/v1/metadata")
21 | assert response.json_body["metadata"]["passwordlessAccess"]
22 |
23 |
24 | def test_can_get_correct_metadata_from_environment():
25 | app = webtest.TestApp(adaero.main({}, **DEFAULT_TEST_SETTINGS))
26 | response = app.get("/api/v1/metadata")
27 | assert not response.json_body["metadata"]["passwordlessAccess"]
28 | os.environ["ALLOW_PASSWORDLESS_ACCESS"] = "true"
29 | response = app.get("/api/v1/metadata")
30 | assert response.json_body["metadata"]["passwordlessAccess"]
31 |
32 |
33 | def test_can_get_correct_logo(): # noqa: E501
34 | dev_settings = copy.copy(DEFAULT_TEST_SETTINGS)
35 | dev_settings[adaero.constants.LOGO_FILENAME_KEY] = "test.png"
36 | app = webtest.TestApp(adaero.main({}, **dev_settings))
37 | resp = app.get('/api/v1/logo.png')
38 | assert resp.status_code == 302
39 | assert resp.headers['Location'] == 'https://localhost/assets/%s' \
40 | % dev_settings[adaero.constants.LOGO_FILENAME_KEY]
41 |
--------------------------------------------------------------------------------
/tests/scripts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/tests/scripts/__init__.py
--------------------------------------------------------------------------------
/tests/scripts/configure_db.py:
--------------------------------------------------------------------------------
1 | import click
2 | from faker import Faker
3 | import transaction
4 |
5 | from adaero.models import (
6 | get_session_factory,
7 | get_tm_session,
8 | Period,
9 | User,
10 | Nominee,
11 | generate_period_dates,
12 | )
13 | from adaero.scripts.configure_db import (
14 | cli,
15 | ENGINE_KEY,
16 | SUBPERIOD_CHOICES,
17 | SUBPERIOD_KEY,
18 | )
19 |
20 | from tests.integration.views.conftest import add_template, add_test_period_with_template
21 |
22 | fake = Faker()
23 |
24 |
25 | @cli.command()
26 | @click.pass_context
27 | def add_test_periods(ctx):
28 | engine = ctx.obj[ENGINE_KEY]
29 | session_factory = get_session_factory(engine)
30 | dbsession = get_tm_session(session_factory, transaction.manager)
31 | QUESTION_IDS_AND_TEMPLATES = [
32 | (1, u"What should {display_name} CONTINUE doing?", None),
33 | (10, u"What should {display_name} STOP doing or do less of?", None),
34 | (3, u"What should {display_name} START doing or do more of?", None),
35 | (
36 | 7,
37 | u"What is the general perception of {display_name}?",
38 | u"There are often general perceptions of a person that are not "
39 | u"always based on your direct experiences. This is can still be "
40 | u"very helpful input to provide.",
41 | ),
42 | ]
43 | template_id = add_template(dbsession, QUESTION_IDS_AND_TEMPLATES, 1)
44 | add_test_period_with_template(
45 | dbsession,
46 | Period.ENROLLMENT_SUBPERIOD,
47 | template_id,
48 | 1,
49 | u"Q1 2018",
50 | add_nominees=False,
51 | )
52 | add_test_period_with_template(
53 | dbsession,
54 | Period.ENROLLMENT_SUBPERIOD,
55 | template_id,
56 | 2,
57 | u"Q4 2017",
58 | offset_from_utc_now_days=-30,
59 | add_nominees=False,
60 | )
61 | add_test_period_with_template(
62 | dbsession,
63 | Period.ENROLLMENT_SUBPERIOD,
64 | template_id,
65 | 3,
66 | u"Q3 2017",
67 | offset_from_utc_now_days=-60,
68 | add_nominees=False,
69 | )
70 | add_test_period_with_template(
71 | dbsession,
72 | Period.ENROLLMENT_SUBPERIOD,
73 | template_id,
74 | 4,
75 | u"Q2 2017",
76 | offset_from_utc_now_days=-90,
77 | add_nominees=False,
78 | )
79 | add_test_period_with_template(
80 | dbsession,
81 | Period.ENROLLMENT_SUBPERIOD,
82 | template_id,
83 | 5,
84 | u"Q1 2017",
85 | offset_from_utc_now_days=-120,
86 | add_nominees=False,
87 | )
88 | add_test_period_with_template(
89 | dbsession,
90 | Period.ENROLLMENT_SUBPERIOD,
91 | template_id,
92 | 6,
93 | u"Q4 2016",
94 | offset_from_utc_now_days=-150,
95 | add_nominees=False,
96 | )
97 |
98 |
99 | @cli.command()
100 | @click.pass_context
101 | def reset_email_flags(ctx):
102 | engine = ctx.obj[ENGINE_KEY]
103 | session_factory = get_session_factory(engine)
104 | dbsession = get_tm_session(session_factory, transaction.manager)
105 | with transaction.manager:
106 | period = Period.get_current_period(dbsession)
107 | print("resetting email flags for period {.name}")
108 | period.enrol_email_last_sent = None
109 | period.enrol_reminder_email_last_sent = None
110 | period.entry_email_last_sent = None
111 | period.entry_reminder_email_last_sent = None
112 | period.review_email_last_sent = None
113 | period.feedback_available_mail_last_sent = None
114 | dbsession.merge(period)
115 |
116 |
117 | if __name__ == "__main__":
118 | cli()
119 |
--------------------------------------------------------------------------------
/tests/settings.py:
--------------------------------------------------------------------------------
1 | from tests.integration import constants
2 |
3 | DEFAULT_TEST_SETTINGS = {
4 | "ahl.web.debug_all": True,
5 | "adaero.use_local_sqlite3": True,
6 | "adaero.company_name": "Example Org.",
7 | "adaero.business_unit": "Alpha",
8 | "adaero.homebase_location": "London",
9 | "adaero.production_hostname": constants.TEST_PRODUCTION_HOSTNAME,
10 | "adaero.production_user": constants.TEST_PRODUCTION_USER,
11 | "adaero.served_on_https": True,
12 | "adaero.talent_manager_usernames": '["llovelace"]',
13 | "adaero.reload_users_on_app_start": False,
14 | "adaero.load_talent_managers_on_app_start": False,
15 | "adaero.staff_override_usernames": None,
16 | "adaero.support_email": "support@example.org",
17 | "adaero.reply_email": "no-reply@example.org",
18 | "adaero.ldap_uri": "ldap://localhost:9999",
19 | "adaero.ldap_user_bind_template": "{username}@localhost.test",
20 | "adaero.ldap_search_bind_dn": "admin",
21 | "adaero.ldap_search_password": "admin_pw",
22 | "adaero.ldap_username_key": constants.TEST_USERNAME_KEY,
23 | "adaero.ldap_manager_key": constants.TEST_MANAGER_KEY,
24 | "adaero.ldap_location_key": constants.TEST_LOCATION_KEY,
25 | "adaero.ldap_uid_key": constants.TEST_UID_KEY,
26 | "adaero.ldap_department_key": constants.TEST_DEPARTMENT_KEY,
27 | "adaero.ldap_business_unit_key": constants.TEST_BUSINESS_UNIT_KEY,
28 | "adaero.ldap_base_dn": "O=foo",
29 | "adaero.ldap_dn_username_attribute": "cn",
30 | "adaero.ldap_dn_username_regex": r"CN=([\w\s\\\,\(\)]*),OU=",
31 | "adaero.enable_send_email": True,
32 | "beaker.session.type": "memory",
33 | "beaker.session.secret": "secret",
34 | "rest_toolkit.debug": True,
35 | }
36 |
--------------------------------------------------------------------------------
/tests/unit/README.md:
--------------------------------------------------------------------------------
1 | # Unit Tests
2 |
3 | Unit tests for this package
4 |
--------------------------------------------------------------------------------
/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/man-group/adaero/991515895fd0063662755e0051577e1b7053a9fd/tests/unit/__init__.py
--------------------------------------------------------------------------------
/tests/unit/population.csv:
--------------------------------------------------------------------------------
1 | first_name,last_name,position,employee_id,business_unit,email,department,is_staff,manager_id
2 | Alice,Alison,Marketing Co-Ordinator,939393,Bravo,aalison@example.com,App Development,True,
3 | Barney,Boggs,Developer,123456,Alpha,bboggs@example.com,App Development,True,321321
4 | Charles,Dalton,Boss,321321,Alpha,cdalton@example.com,App Development,True,999999
5 | Dominic,Dodson,Developer,878787,Alpha,ddodson@example.com,App Development,True,321321
6 | Other,Employee,OE,102938,Alpha,otheremployee@example.com,Compliance - Bravo,True,102939
7 | Lady,Lovelace,Bigger boss,999999,Alpha,llovelace@example.com,App Development,True,135790
8 | Central,Manager,CEO,135790,Central,centralmanager@man.com,Central Management,True,
9 | Other,Manager,OM,102939,Alpha,othermanager@example.com,Compliance - Bravo,True,102939
10 | Șarah,Sholes,Business Manager,123123,Alpha,ssholes@example.com,App Development,True,321321
11 | Outside,TM,Talent Manager,989898,HR,outsidetm@man.com,Human Resources,True,
--------------------------------------------------------------------------------
/tests/unit/test_config.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import pyramid.testing
3 | import os
4 |
5 | from adaero import config
6 | from adaero import constants
7 |
8 | DEFAULT_SETTINGS = {
9 | constants.CHECK_AND_SEND_EMAIL_INT_KEY: "60",
10 | constants.LOAD_USER_EMAIL_LIST_KEY: '["bob", "dan"]',
11 | constants.ALLOW_PASSWORDLESS_ACCESS_KEY: True,
12 | }
13 |
14 |
15 | @pytest.mark.parametrize(
16 | "envvar, envvar_val, key, expected",
17 | (
18 | (
19 | "CHECK_AND_SEND_EMAIL_INTERVAL_S",
20 | "90",
21 | constants.CHECK_AND_SEND_EMAIL_INT_KEY,
22 | "90",
23 | ),
24 | (None, None, constants.CHECK_AND_SEND_EMAIL_INT_KEY, "60"),
25 | (
26 | "ALLOW_PASSWORDLESS_ACCESS",
27 | "true",
28 | constants.ALLOW_PASSWORDLESS_ACCESS_KEY,
29 | True,
30 | ),
31 | (
32 | "ALLOW_PASSWORDLESS_ACCESS",
33 | "false",
34 | constants.ALLOW_PASSWORDLESS_ACCESS_KEY,
35 | False,
36 | ),
37 | (None, None, constants.ALLOW_PASSWORDLESS_ACCESS_KEY, True),
38 | (None, None, constants.LOAD_USER_EMAIL_LIST_KEY, '["bob", "dan"]'),
39 | ),
40 | )
41 | def test_get_config_value(envvar, envvar_val, key, expected):
42 | configuration = pyramid.testing.setUp(settings=DEFAULT_SETTINGS)
43 | settings = configuration.get_settings()
44 | if envvar:
45 | os.environ[envvar] = envvar_val
46 | generated = config.get_config_value(settings, key)
47 | assert expected == generated
48 | if envvar:
49 | os.environ.pop(envvar)
50 |
--------------------------------------------------------------------------------
/tests/unit/test_date.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | from __future__ import unicode_literals
3 | from datetime import datetime
4 |
5 | import pytest
6 |
7 | import adaero.date
8 | from adaero.models import User
9 |
10 |
11 | @pytest.mark.parametrize(
12 | "value, man_location, format_, expected",
13 | (
14 | (
15 | datetime(2016, 3, 2, 10, 24, 29, 123),
16 | "London",
17 | "%Y-%m-%d %H:%M (%Z)",
18 | "2016-03-02 10:00 (GMT)",
19 | ),
20 | (
21 | datetime(2016, 3, 2, 10, 29, 0, 0),
22 | "London",
23 | "%Y-%m-%d %H:%M (%Z)",
24 | "2016-03-02 10:00 (GMT)",
25 | ),
26 | (
27 | datetime(2016, 3, 2, 14, 42, 29, 123),
28 | "London",
29 | "%Y-%m-%d %H:%M (%Z)",
30 | "2016-03-02 14:30 (GMT)",
31 | ),
32 | (
33 | datetime(2016, 3, 2, 15, 58, 29, 123),
34 | "London",
35 | "%Y-%m-%d %H:%M (%Z)",
36 | "2016-03-02 15:30 (GMT)",
37 | ),
38 | (
39 | datetime(2016, 3, 2, 15, 59, 29, 123),
40 | "London",
41 | "%Y-%m-%d %H:%M (%Z)",
42 | "2016-03-02 15:30 (GMT)",
43 | ),
44 | (
45 | datetime(2016, 3, 2, 16, 0, 0, 0),
46 | "London",
47 | "%Y-%m-%d %H:%M (%Z)",
48 | "2016-03-02 16:00 (GMT)",
49 | ),
50 | (
51 | datetime(2016, 3, 2, 16, 1, 0, 0),
52 | "London",
53 | "%Y-%m-%d %H:%M (%Z)",
54 | "2016-03-02 16:00 (GMT)",
55 | ),
56 | # daylight savings
57 | (
58 | datetime(2017, 3, 26, 0, 59, 0, 0),
59 | "London",
60 | "%Y-%m-%d %H:%M (%Z)",
61 | "2017-03-26 00:30 (GMT)",
62 | ),
63 | (
64 | datetime(2017, 3, 26, 9, 9, 0, 0),
65 | "London",
66 | "%Y-%m-%d %H:%M (%Z)",
67 | "2017-03-26 09:00 (BST)",
68 | ),
69 | (
70 | datetime(2017, 3, 26, 2, 5, 0, 0),
71 | "London",
72 | "%Y-%m-%d %H:%M (%Z)",
73 | "2017-03-26 02:00 (BST)",
74 | ),
75 | # all other locations
76 | (
77 | datetime(2018, 1, 11, 19, 30, 29, 123),
78 | "Hong Kong",
79 | "%Y-%m-%d %H:%M (%Z)",
80 | "2018-01-11 19:30 (HKT)",
81 | ),
82 | (
83 | datetime(2018, 1, 11, 6, 35, 29, 123),
84 | "New York",
85 | "%Y-%m-%d %H:%M (%Z)",
86 | "2018-01-11 06:30 (EST)",
87 | ),
88 | (
89 | datetime(2018, 1, 11, 11, 50, 29, 123),
90 | "Oxford",
91 | "%Y-%m-%d %H:%M (%Z)",
92 | "2018-01-11 11:30 (GMT)",
93 | ),
94 | (
95 | datetime(2018, 1, 11, 19, 50, 29, 123),
96 | "Shanghai",
97 | "%Y-%m-%d %H:%M (%Z)",
98 | "2018-01-11 19:30 (CST)",
99 | ),
100 | (
101 | datetime(2018, 1, 11, 11, 50, 29, 123),
102 | "GBR",
103 | "%Y-%m-%d %H:%M (%Z)",
104 | "2018-01-11 11:30 (GMT)",
105 | ),
106 | (
107 | datetime(2018, 1, 11, 6, 50, 29, 123),
108 | "Boston",
109 | "%Y-%m-%d %H:%M (%Z)",
110 | "2018-01-11 06:30 (EST)",
111 | ),
112 | (
113 | datetime(2018, 1, 11, 11, 50, 29, 123),
114 | None,
115 | "%Y-%m-%d %H:%M (%Z)",
116 | "2018-01-11 11:30 (GMT)",
117 | ),
118 | (datetime(2018, 1, 3, 0, 0, 0, 0), None, None, "2 January 18:00 (GMT)"),
119 | (datetime(2017, 3, 26, 0, 0, 0, 0), "Boston", None, "25 March 18:00 (EDT)"),
120 | (datetime(2018, 3, 26, 0, 0, 0, 0), "Pfäffikon", None, "25 March 18:00 (CET)"),
121 | (
122 | datetime(2018, 3, 26, 11, 0, 0, 0),
123 | "Pfäffikon",
124 | None,
125 | "25 March 18:00 (CEST)",
126 | ),
127 | (datetime(2018, 1, 3, 9, 0, 0, 0), None, None, "2 January 18:00 (GMT)"),
128 | (datetime(2017, 3, 26, 10, 0, 0, 0), "Boston", None, "25 March 18:00 (EDT)"),
129 | (
130 | datetime(2016, 3, 2, 10, 24, 29, 123),
131 | "Invalid location",
132 | "%Y-%m-%d %H:%M (%Z)",
133 | "2016-03-02 10:00 (GMT)",
134 | ),
135 | ),
136 | )
137 | def test_datetimeformat_works(value, man_location, format_, expected):
138 | user = User(username="foo", location=man_location)
139 | if format_:
140 | assert expected == adaero.date.datetimeformat(value, user, format_)
141 | else:
142 | assert expected == adaero.date.datetimeformat(value, user)
143 |
--------------------------------------------------------------------------------
/tests/unit/test_period.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta, time
2 |
3 | from freezegun import freeze_time
4 | import pytest
5 |
6 | import adaero.date
7 | from adaero.models.period import Period
8 |
9 | TEST_YEAR = 2017
10 | TEST_MONTH = 2
11 | TEST_DAY = 15
12 | TEST_DATETIME = datetime(year=TEST_YEAR, month=TEST_MONTH, day=TEST_DAY)
13 |
14 | TEST_DAYLIGHT_SAVINGS = datetime(year=2017, month=4, day=15)
15 |
16 |
17 | def date_offset_by(days, hours, mins, dt=TEST_DATETIME):
18 | return dt + timedelta(days=days, hours=hours, minutes=mins)
19 |
20 |
21 | @pytest.mark.parametrize(
22 | "period, expected, location, time, dt",
23 | [
24 | (
25 | Period(
26 | enrollment_start_utc=date_offset_by(0, 9, 0),
27 | entry_start_utc=date_offset_by(2, 9, 0),
28 | approval_start_utc=date_offset_by(3, 9, 0),
29 | approval_end_utc=date_offset_by(4, 9, 0),
30 | ),
31 | Period.INACTIVE_SUBPERIOD,
32 | adaero.date.LONDON,
33 | time(hour=8, minute=59),
34 | TEST_DATETIME,
35 | ),
36 | # daylight savings
37 | (
38 | Period(
39 | enrollment_start_utc=date_offset_by(0, 9, 0, TEST_DAYLIGHT_SAVINGS),
40 | entry_start_utc=date_offset_by(2, 9, 0, TEST_DAYLIGHT_SAVINGS),
41 | approval_start_utc=date_offset_by(3, 9, 0, TEST_DAYLIGHT_SAVINGS),
42 | approval_end_utc=date_offset_by(4, 9, 0, TEST_DAYLIGHT_SAVINGS),
43 | ),
44 | Period.ENROLLMENT_SUBPERIOD,
45 | adaero.date.LONDON,
46 | time(hour=8, minute=59),
47 | TEST_DAYLIGHT_SAVINGS,
48 | ),
49 | (
50 | Period(
51 | enrollment_start_utc=date_offset_by(0, 9, 0),
52 | entry_start_utc=date_offset_by(1, 9, 0),
53 | approval_start_utc=date_offset_by(2, 9, 0),
54 | approval_end_utc=date_offset_by(3, 9, 0),
55 | ),
56 | Period.ENROLLMENT_SUBPERIOD,
57 | adaero.date.LONDON,
58 | time(hour=9, minute=00),
59 | TEST_DATETIME,
60 | ),
61 | (
62 | Period(
63 | enrollment_start_utc=date_offset_by(-1, 9, 0),
64 | entry_start_utc=date_offset_by(0, 9, 0),
65 | approval_start_utc=date_offset_by(2, 9, 0),
66 | approval_end_utc=date_offset_by(3, 9, 0),
67 | ),
68 | Period.ENROLLMENT_SUBPERIOD,
69 | adaero.date.LONDON,
70 | time(hour=8, minute=59),
71 | TEST_DATETIME,
72 | ),
73 | (
74 | Period(
75 | enrollment_start_utc=date_offset_by(-2, 9, 0),
76 | entry_start_utc=date_offset_by(0, 9, 0),
77 | approval_start_utc=date_offset_by(1, 9, 0),
78 | approval_end_utc=date_offset_by(2, 9, 0),
79 | ),
80 | Period.ENTRY_SUBPERIOD,
81 | adaero.date.LONDON,
82 | time(hour=9, minute=59),
83 | TEST_DATETIME,
84 | ),
85 | (
86 | Period(
87 | enrollment_start_utc=date_offset_by(-10, 9, 0),
88 | entry_start_utc=date_offset_by(-9, 9, 0),
89 | approval_start_utc=date_offset_by(0, 9, 0),
90 | approval_end_utc=date_offset_by(3, 9, 0),
91 | ),
92 | Period.ENTRY_SUBPERIOD,
93 | adaero.date.BOSTON,
94 | time(hour=9, minute=59),
95 | TEST_DATETIME,
96 | ),
97 | (
98 | Period(
99 | enrollment_start_utc=date_offset_by(-10, 9, 0),
100 | entry_start_utc=date_offset_by(-9, 9, 0),
101 | approval_start_utc=date_offset_by(0, 9, 0),
102 | approval_end_utc=date_offset_by(3, 9, 0),
103 | ),
104 | Period.APPROVAL_SUBPERIOD,
105 | adaero.date.BOSTON,
106 | time(hour=14, minute=59),
107 | TEST_DATETIME,
108 | ),
109 | (
110 | Period(
111 | enrollment_start_utc=date_offset_by(-100, 9, 0),
112 | entry_start_utc=date_offset_by(-99, 9, 0),
113 | approval_start_utc=date_offset_by(-90, 9, 0),
114 | approval_end_utc=date_offset_by(0, 9, 0),
115 | ),
116 | Period.APPROVAL_SUBPERIOD,
117 | adaero.date.HONG_KONG,
118 | time(hour=0, minute=12),
119 | TEST_DATETIME,
120 | ),
121 | (
122 | Period(
123 | enrollment_start_utc=date_offset_by(-100, 9, 0),
124 | entry_start_utc=date_offset_by(-99, 9, 0),
125 | approval_start_utc=date_offset_by(-90, 9, 0),
126 | approval_end_utc=date_offset_by(0, 9, 0),
127 | ),
128 | Period.REVIEW_SUBPERIOD,
129 | adaero.date.HONG_KONG,
130 | time(hour=1, minute=10),
131 | TEST_DATETIME,
132 | ),
133 | ],
134 | )
135 | def test_subperiod_is_correct(period, expected, location, time, dt):
136 | dt = datetime.combine(dt, time)
137 | with freeze_time(dt):
138 | assert expected == period.subperiod(location)
139 |
--------------------------------------------------------------------------------
/tests/unit/test_text.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from datetime import datetime
3 |
4 | from freezegun import freeze_time
5 | import pytest
6 |
7 | from adaero.constants import ANSWER_CHAR_LIMIT
8 | from adaero.text import check_input
9 |
10 | TEST_DATE = datetime(2017, 10, 6, 22, 5)
11 |
12 |
13 | @freeze_time(TEST_DATE)
14 | @pytest.mark.parametrize(
15 | "input_, expected",
16 | (
17 | (u"yes \n\n\n", None),
18 | (
19 | u"x" * (ANSWER_CHAR_LIMIT + 32),
20 | u"%s: Character limit of %s has been "
21 | u"exceeded by %s. Please reduce your "
22 | u"answer size." % (TEST_DATE, ANSWER_CHAR_LIMIT, 32),
23 | ),
24 | ),
25 | )
26 | def test_check_input(input_, expected):
27 | assert expected == check_input(input_)
28 |
--------------------------------------------------------------------------------