├── .babelrc
├── .devcontainer
├── Dockerfile
├── devcontainer.json
├── docker-compose.yml
├── requirements.txt.temp
└── settings.vscode.json
├── .dockerignore
├── .env-sample
├── .gitignore
├── .vscode
├── launch.json
└── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── SECURITY.md
├── azure-ci-pipeline.yml
├── azure-deploy-pipeline.yml
├── docker-compose.yml
├── manage.py
├── package-lock.json
├── package.json
├── pytest.ini
├── requirements.pipelines.txt
├── requirements.txt
├── scripts
├── create_db.py
├── post_build.sh
└── psql.py
├── tweeter
├── .gitignore
├── __init__.py
├── admin.py
├── apps.py
├── fixtures
│ └── initial_data.json
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── permissions.py
├── serializers.py
├── src
│ ├── components
│ │ ├── App.css
│ │ ├── App.js
│ │ ├── Auth.js
│ │ ├── DataProvider.js
│ │ ├── Tweets.css
│ │ └── Tweets.js
│ └── index.js
├── templates
│ ├── registration
│ │ └── login.html
│ ├── signup.html
│ └── tweeter
│ │ └── index.html
├── tests.py
└── views.py
├── tweeter3
├── __init__.py
├── settings
│ ├── __init__.py
│ ├── base.py
│ ├── devcontainer.py
│ ├── development.py
│ └── production.py
├── urls.py
└── wsgi.py
├── uwsgi.ini
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "presets": [
4 | "@babel/preset-env", "@babel/preset-react",
5 | ],
6 | // "presets": [
7 | // [
8 | // "latest", {
9 | // "es2015": {
10 | // "modules": false
11 | // }
12 | // }
13 | // ]
14 | // ],
15 | "plugins": [
16 | "transform-class-properties", "@babel/transform-runtime"
17 | ]
18 | }
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | #-----------------------------------------------------------------------------------------
2 | # Copyright (c) Microsoft Corporation. All rights reserved.
3 | # Licensed under the MIT License. See LICENSE in the project root for license information.
4 | #-----------------------------------------------------------------------------------------
5 |
6 | FROM python:3
7 |
8 | # Copy default endpoint specific user settings overrides into container to specify Python path
9 | COPY .devcontainer/settings.vscode.json /root/.vscode-remote/data/Machine/settings.json
10 |
11 | ENV PYTHONUNBUFFERED 1
12 |
13 | RUN mkdir /workspace
14 | WORKDIR /workspace
15 |
16 | ENV SHELL /bin/bash
17 |
18 | # Install node for building the front end
19 | RUN apt-get update
20 | RUN apt-get -y install curl gnupg
21 | RUN curl -sL https://deb.nodesource.com/setup_11.x | bash -
22 | RUN apt-get -y install nodejs
23 |
24 | # Install git, process tools
25 | RUN apt-get update && apt-get -y install git procps
26 |
27 | # Install Python dependencies from requirements.txt if it exists
28 | COPY .devcontainer/requirements.txt.temp requirements.txt* /workspace/
29 | RUN if [ -f "requirements.txt" ]; then pip install -r requirements.txt && rm requirements.txt; fi
30 |
31 | # Clean up
32 | RUN apt-get autoremove -y \
33 | && apt-get clean -y \
34 | && rm -rf /var/lib/apt/lists/*
35 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Python 3 & PostgreSQL",
3 | "dockerComposeFile": "docker-compose.yml",
4 | "service": "app",
5 | "workspaceFolder": "/workspace",
6 | "extensions": [
7 | "ms-python.python",
8 | "VisualStudioExptTeam.vscodeintellicode"
9 | ]
10 | }
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | app:
5 | build:
6 | context: ..
7 | dockerfile: .devcontainer/Dockerfile
8 | ports:
9 | - 8000:8000
10 | volumes:
11 | - ~/.gitconfig:/root/.gitconfig
12 | - ..:/workspace
13 | command: sleep infinity
14 | environment:
15 | DJANGO_SETTINGS_MODULE: tweeter3.settings.devcontainer
16 |
17 | db:
18 | image: postgres
19 | restart: always
20 | ports:
21 | - 5432:5432
22 | environment:
23 | POSTGRES_PASSWORD: LocalPassword
--------------------------------------------------------------------------------
/.devcontainer/requirements.txt.temp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/python-sample-tweeterapp/b61b19dd30d5320253aa1fa28f7a2fb515c1124e/.devcontainer/requirements.txt.temp
--------------------------------------------------------------------------------
/.devcontainer/settings.vscode.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.pythonPath": "/usr/local/bin/python",
3 | "_workbench.uiExtensions" : [ "peterjausovec.vscode-docker" ],
4 | "python.jediEnabled": false
5 | }
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | Dockerfile*
4 | docker-compose*
5 | .dockerignore
6 | .git
7 | .gitignore
8 | .env
9 | */bin
10 | */obj
11 | README.md
12 | LICENSE
13 | .vscode
14 | env
15 | static/frontend
16 | **/__pycache__
17 | **/*.pyc
--------------------------------------------------------------------------------
/.env-sample:
--------------------------------------------------------------------------------
1 | # Sample Production Environment Variable File
2 | # These variables should be set in App Service App Settings
3 | # See: https://docs.microsoft.com/en-us/azure/app-service/web-sites-configure#app-settings
4 |
5 | # Set the Production Settings Module
6 | DJANGO_SETTINGS_MODULE=tweeter3.settings.production
7 |
8 | # Configure the SECRET_KEY for Django See: https://docs.djangoproject.com/en/2.1/ref/settings/#secret-key
9 | SECRET_KEY=my-secret-key
10 |
11 | # For deployment purposes, configure the Azure App Service Hostname
12 | AZURE_HOSTNAME=myhost
13 |
14 | # App Service Build Settings (not used in Docker scenario)
15 | POST_BUILD_SCRIPT_PATH=scripts/pre_deploy.sh
16 | # App Service Port setting
17 | WEBSITES_PORT=8000
18 |
19 | # Configure the PostgreSQL Database with App Specific Settings
20 | DB_USER=db_user
21 | DB_PASSWORD=db_password
22 | DB_NAME=db_name
23 | DB_HOST=db_host
24 |
25 | # Required for error emails
26 | SEND_ADMIN_EMAILS=true
27 | EMAIL_HOST_USER=your-email-account
28 | EMAIL_HOST_PASSWORD=your-email-password
29 | ADMIN_EMAIL_TO=email-target-account
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | #lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .nox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | .hypothesis/
49 | .pytest_cache/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 | db.sqlite3
59 | staticfiles/
60 | .env*
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # IPython
79 | profile_default/
80 | ipython_config.py
81 |
82 | # pyenv
83 | .python-version
84 |
85 | # celery beat schedule file
86 | celerybeat-schedule
87 |
88 | # SageMath parsed files
89 | *.sage.py
90 |
91 | # Environments
92 | .env
93 | .venv
94 | env/
95 | venv/
96 | ENV/
97 | env.bak/
98 | venv.bak/
99 |
100 | # Spyder project settings
101 | .spyderproject
102 | .spyproject
103 |
104 | # Rope project settings
105 | .ropeproject
106 |
107 | # mkdocs documentation
108 | /site
109 |
110 | # mypy
111 | .mypy_cache/
112 | .dmypy.json
113 | dmypy.json
114 | node_modules
115 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Python: Django",
9 | "type": "python",
10 | "request": "launch",
11 | "program": "${workspaceFolder}/manage.py",
12 | "args": [
13 | "runserver",
14 | "--noreload",
15 | "--nothreading"
16 | ],
17 | "django": true
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.testing.pytestArgs": [
3 | "tweeter"
4 | ],
5 | "python.testing.unittestEnabled": false,
6 | "python.testing.nosetestsEnabled": false,
7 | "python.testing.pytestEnabled": true
8 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build node front-end in a node container
2 | FROM node
3 |
4 | WORKDIR /nodebuild
5 | ADD . /nodebuild
6 | RUN npm install
7 | RUN npm run build
8 |
9 | # Use a python uwsgi nginx image for the runtime image (no node runtime here)
10 | FROM tiangolo/uwsgi-nginx
11 |
12 | ENV LISTEN_PORT=8000
13 | EXPOSE 8000
14 |
15 | # Indicate where uwsgi.ini lives
16 | ENV UWSGI_INI uwsgi.ini
17 |
18 | # Tell nginx where static files live
19 | ENV STATIC_URL /app/tweeter3/staticfiles
20 |
21 | WORKDIR /app
22 | COPY --from=0 /nodebuild /app
23 |
24 | # Install pip requirements and collect django static files
25 | RUN python3 -m pip install -r requirements.txt
26 | RUN python3 manage.py collectstatic --noinput
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [year] [fullname]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## Getting Started with Dev Containers
3 |
4 | 1. Get set up with Visual Studio Code insiders, Docker and remote extensions [instructions here](https://vscode-docs-remote.azurewebsites.net/docs/remote/remote-overview#_getting-started)
5 |
6 | 1. If on windows, set git to use LF line endings:
7 | ```
8 | git config --global core.autocrlf false
9 | ```
10 | 1. Clone repo
11 | ```
12 | git clone https://github.com/Microsoft/python-sample-tweeterapp
13 | ```
14 | 1. From Visual Studio Code Insiders, run the ```Remote-Containers: Open Folder in Container...``` and select the ```python-sample-tweeterapp``` folder
15 |
16 | After the dev container has installed and files appear in the explorer, you are good to go!
17 |
18 | ## Run some code!
19 |
20 |
21 | Build the node front end by opening a new terminal with ```Ctrl-Shift-` ```, and running:
22 |
23 | ```
24 | npm install
25 | npm run dev
26 | ```
27 |
28 |
29 | Start the Django server by opening a new terminal (```Ctrl-Shift-` ```), and running:
30 |
31 | ```
32 | python manage.py migrate
33 | python manage.py loaddata initial_data
34 | python manage.py runserver
35 | ```
36 |
37 | ## Presentations
38 |
39 | * Build 2019: Building Python Web Applications with Visual Studio Code, Docker, and Azure
40 | * [[slides](https://1drv.ms/p/s!Ak36tGOBftKVv0ga1kcCTiPXhuff)] [[Sentiment API Code](https://github.com/qubitron/qsentiment)]
41 |
42 | # Contributing
43 |
44 | This project welcomes contributions and suggestions. Most contributions require you to agree to a
45 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
46 | the rights to use your contribution. For details, visit https://cla.microsoft.com.
47 |
48 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
49 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
50 | provided by the bot. You will only need to do this once across all repos using our CLA.
51 |
52 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
53 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
54 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
55 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/azure-ci-pipeline.yml:
--------------------------------------------------------------------------------
1 | # Run Python pytests
2 | # https://docs.microsoft.com/azure/devops/pipelines/languages/python
3 | trigger:
4 | - master
5 |
6 | pool:
7 | vmImage: 'Ubuntu 16.04'
8 | strategy:
9 | matrix:
10 | Python37:
11 | PYTHON_VERSION: '3.7'
12 | Python36:
13 | PYTHON_VERSION: '3.6'
14 | maxParallel: 3
15 |
16 | steps:
17 | - task: UsePythonVersion@0
18 | inputs:
19 | versionSpec: '$(PYTHON_VERSION)'
20 | architecture: 'x64'
21 | - script: |
22 | python -m pip install --upgrade pip
23 | pip install -r requirements.pipelines.txt
24 | pip install pytest pytest-azurepipelines
25 | displayName: 'Install dependencies'
26 | - script: |
27 | python -m pytest .
28 | env:
29 | DJANGO_SETTINGS_MODULE: tweeter3.settings.development
30 | displayName: 'pytest'
--------------------------------------------------------------------------------
/azure-deploy-pipeline.yml:
--------------------------------------------------------------------------------
1 | # Docker image
2 | # Build a Docker image to deploy, run, or push to a container registry.
3 | # Add steps that use Docker Compose, tag images, push to a registry, run an image, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/docker
5 |
6 | pool:
7 | vmImage: 'Ubuntu-16.04'
8 |
9 | variables:
10 | imageName: 'pythondemos.azurecr.io/tweeter-app:latest'
11 | containerName: 'tweeterapp'
12 | steps:
13 | - script: docker login pythondemos.azurecr.io -u pythondemos -p $DOCKER_LOGIN_PASSWORD
14 | env:
15 | DOCKER_LOGIN_PASSWORD: $(ACR_KEY)
16 | displayName: 'docker login'
17 | - script: docker build -f Dockerfile -t $(imageName) .
18 | displayName: 'docker build'
19 | - script: docker run --name $(containerName) --detach -e DJANGO_SETTINGS_MODULE=tweeter3.settings.production -e DB_USER=$DB_USER -e DB_PASSWORD=$DB_PASSWORD -e DB_NAME=$DB_NAME -e DB_HOST=$DB_HOST $(imageName)
20 | env:
21 | DB_USER: $(DB_USER)
22 | DB_PASSWORD: $(DB_PASSWORD)
23 | DB_NAME: $(DB_NAME)
24 | DB_HOST: $(DB_HOST)
25 | displayName: 'start container'
26 | - script: docker exec $(containerName) python3 manage.py migrate
27 | displayName: run django migrations
28 | - script: docker push $(imageName)
29 | displayName: 'docker push'
30 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2.1'
2 |
3 | services:
4 | tweeter3:
5 | image: pythondemos.azurecr.io/tweeter-app
6 | container_name: tweeterapp
7 | build: .
8 | env_file: .env
9 | ports:
10 | - 8000:8000
11 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 | import dotenv
5 |
6 | if __name__ == '__main__':
7 | # Use .env file if it is found
8 | env_path = os.getcwd() + '/.env'
9 | if os.path.exists(env_path):
10 | dotenv.read_dotenv(env_path)
11 |
12 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tweeter3.settings.development')
13 | if (os.environ.get('DJANGO_SETTINGS_MODULE') == 'tweeter3.settings.devcontainer'):
14 | from django.core.management.commands.runserver import Command as runserver
15 | runserver.default_addr = "0.0.0.0"
16 |
17 | try:
18 | from django.core.management import execute_from_command_line
19 | except ImportError as exc:
20 | raise ImportError(
21 | "Couldn't import Django. Are you sure it's installed and "
22 | "available on your PYTHONPATH environment variable? Did you "
23 | "forget to activate a virtual environment?"
24 | ) from exc
25 | execute_from_command_line(sys.argv)
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tweeterapp",
3 | "version": "1.0.0",
4 | "description": "tweeter3 =======",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "webpack-dev-server --hot --inline",
9 | "dev": "webpack --watch --mode development ./tweeter/src/index.js --output ./tweeter/static/frontend/main.js",
10 | "build": "webpack --mode production ./tweeter/src/index.js --output ./tweeter/static/frontend/main.js"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/qubitron/tweeterapp.git"
15 | },
16 | "keywords": [],
17 | "author": "",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/qubitron/tweeterapp/issues"
21 | },
22 | "homepage": "https://github.com/qubitron/tweeterapp#readme",
23 | "devDependencies": {
24 | "@babel/core": "^7.4.3",
25 | "@babel/plugin-transform-runtime": "^7.4.4",
26 | "@babel/preset-env": "^7.4.3",
27 | "@babel/preset-react": "^7.0.0",
28 | "antd": "^3.16.5",
29 | "autoprefixer": "^9.5.1",
30 | "babel-loader": "^8.0.5",
31 | "babel-plugin-transform-class-properties": "^6.24.1",
32 | "babel-preset-latest": "^6.24.1",
33 | "css-loader": "^2.1.1",
34 | "file-loader": "^3.0.1",
35 | "postcss-flexbugs-fixes": "^4.1.0",
36 | "postcss-loader": "^3.0.0",
37 | "postcss-normalize": "^7.0.1",
38 | "prop-types": "^15.7.2",
39 | "react": "^16.8.6",
40 | "react-dom": "^16.8.6",
41 | "react-router-dom": "^5.0.0",
42 | "weak-key": "^1.0.1",
43 | "webpack": "^4.30.0",
44 | "webpack-cli": "^3.3.12",
45 | "webpack-dev-server": "^3.11.0"
46 | },
47 | "dependencies": {
48 | "@babel/runtime": "^7.4.4",
49 | "babel-preset-es2015": "^6.24.1",
50 | "js-cookie": "^2.2.0",
51 | "react-hot-loader": "^4.8.4",
52 | "style-loader": "^0.23.1"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 |
3 | DJANGO_SETTINGS_MODULE = tweeter3.settings
4 |
5 | python_files = tests.py test_*.py *_tests.py
6 |
7 | addopts = -p no:warnings
--------------------------------------------------------------------------------
/requirements.pipelines.txt:
--------------------------------------------------------------------------------
1 | astroid==2.0.4
2 | Django>=2.1.6
3 | python-dotenv
4 | djangorestframework==3.9.1
5 | isort==4.3.4
6 | lazy-object-proxy==1.3.1
7 | mccabe==0.6.1
8 | psycopg2-binary==2.7.5
9 | # Use the following in docker to work around: https://github.com/unbit/uwsgi/issues/1569
10 | #psycopg2==2.7.4 --no-binary psycopg2
11 | pycodestyle==2.4.0
12 | pytz==2018.5
13 | six==1.11.0
14 | whitenoise==4.1
15 | wrapt==1.10.11
16 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | astroid==2.0.4
2 | Django>=2.2
3 | python-dotenv
4 | djangorestframework==3.11.0
5 | isort==4.3.4
6 | lazy-object-proxy==1.3.1
7 | mccabe==0.6.1
8 | psycopg2==2.8.5 --no-binary psycopg2
9 | pycodestyle==2.4.0
10 | pytz==2018.5
11 | six==1.11.0
12 | whitenoise==5.1.0
13 | wrapt==1.10.11
14 |
--------------------------------------------------------------------------------
/scripts/create_db.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 | import urllib.request
4 | import dotenv
5 |
6 | # Load dotenv file
7 | dotenv.load_dotenv()
8 |
9 | REQUIRED_ENV_VARS = (
10 | 'DB_USER',
11 | 'DB_PASSWORD',
12 | 'DB_NAME',
13 | 'DB_HOST'
14 | )
15 |
16 | AZ_GROUP=os.getenv('AZ_GROUP', 'appsvc_rg_linux_centralus')
17 | AZ_LOCATION=os.getenv('AZ_LOCATION', 'Central US')
18 | create_group_command = [
19 | "az", "group", "create",
20 | "--name", AZ_GROUP,
21 | "--location", AZ_LOCATION
22 | ]
23 | print("Creating resource group if needed...")
24 | subprocess.call(create_group_command, shell=True)
25 |
26 | missing = []
27 | for v in REQUIRED_ENV_VARS:
28 | if v not in os.environ:
29 | missing.append(v)
30 | if missing:
31 | print("Required Environment Variables Unset:")
32 | print("\t" + "\n\t".join(missing))
33 | print("Exiting.")
34 | exit()
35 |
36 | print("This script will take about 3 minutes to run.")
37 |
38 | # Ref: https://docs.microsoft.com/en-gb/cli/azure/postgres/server?view=azure-cli-latest#az-postgres-server-create
39 | # SKUs: https://docs.microsoft.com/en-us/azure/postgresql/concepts-pricing-tiers
40 | # {pricing tier}_{compute generation}_{vCores}
41 | create_server_command = [
42 | 'az', 'postgres', 'server', 'create',
43 | '--resource-group', AZ_GROUP,
44 | '--location', AZ_LOCATION,
45 | '--name', os.getenv('DB_HOST'),
46 | '--admin-user', os.getenv('DB_USER'),
47 | '--admin-password', os.getenv('DB_PASSWORD'),
48 | '--sku-name', 'B_Gen5_1',
49 | ]
50 |
51 | print("Creating PostgreSQL server...")
52 | subprocess.check_call(create_server_command, shell=True)
53 |
54 | # Set up firewall.
55 | # Ref: https://docs.microsoft.com/en-gb/cli/azure/postgres/server/firewall-rule?view=azure-cli-latest#az-postgres-server-firewall-rule-create
56 | azure_firewall_command = [
57 | 'az', 'postgres', 'server', 'firewall-rule', 'create',
58 | '--resource-group', AZ_GROUP,
59 | '--server-name', os.getenv('DB_HOST'),
60 | '--start-ip-address', '0.0.0.0',
61 | '--end-ip-address', '0.0.0.0',
62 | '--name', 'AllowAllAzureIPs',
63 | ]
64 |
65 | with urllib.request.urlopen('http://ip.42.pl/raw') as f:
66 | my_ip = f.read().decode("utf-8")
67 |
68 | local_ip_firewall_command = [
69 | 'az', 'postgres', 'server', 'firewall-rule', 'create',
70 | '--resource-group', AZ_GROUP,
71 | '--server-name', os.getenv('DB_HOST'),
72 | '--start-ip-address', my_ip,
73 | '--end-ip-address', my_ip,
74 | '--name', 'AllowMyIP',
75 | ]
76 |
77 | print("Allowing access from Azure...")
78 | subprocess.check_call(azure_firewall_command, shell=True)
79 | print("Allowing access from local IP...")
80 | subprocess.check_call(local_ip_firewall_command, shell=True)
81 |
82 | create_db_command = [
83 | 'az', 'postgres', 'db', 'create',
84 | '--resource-group', AZ_GROUP,
85 | '--server-name', os.getenv('DB_HOST'),
86 | '--name', os.getenv('DB_NAME'),
87 | ]
88 |
89 | print("Creating App DB...")
90 | subprocess.check_call(create_db_command, shell=True)
91 |
92 | connect_details_command = [
93 | 'az', 'postgres', 'server', 'show',
94 | '--resource-group', AZ_GROUP,
95 | '--name', os.getenv('DB_HOST'),
96 | ]
97 | print("Getting access details...")
98 | subprocess.check_call(connect_details_command, shell=True)
99 |
--------------------------------------------------------------------------------
/scripts/post_build.sh:
--------------------------------------------------------------------------------
1 | python manage.py migrate
2 |
--------------------------------------------------------------------------------
/scripts/psql.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 | import urllib.request
4 | import dotenv
5 |
6 | dotenv.read_dotenv(os.getcwd() + "/.env")
7 |
8 | REQUIRED_ENV_VARS = (
9 | 'DB_USER',
10 | 'DB_PASSWORD',
11 | 'DB_NAME',
12 | 'DB_HOST'
13 | )
14 |
15 | missing = []
16 | for v in REQUIRED_ENV_VARS:
17 | if v not in os.environ:
18 | missing.append(v)
19 | if missing:
20 | print("Required Environment Variables Unset:")
21 | print("\t" + "\n\t".join(missing))
22 | print("Exiting.")
23 | exit()
24 |
25 | os.environ.setdefault('PGPASSWORD', os.environ.get('DB_PASSWORD'))
26 |
27 | psql_command = [
28 | "psql",
29 | "-v", f"db_password=\"'{os.environ.get('DB_PASSWORD')}",
30 | "-h", f"{os.environ.get('DB_HOST')}.postgres.database.azure.com",
31 | "-U", f"{os.environ.get('DB_USER')}@{os.environ.get('DB_HOST')}",
32 | "postgres"
33 | ]
34 | subprocess.check_call(psql_command, shell=True)
35 |
--------------------------------------------------------------------------------
/tweeter/.gitignore:
--------------------------------------------------------------------------------
1 | static/frontend
--------------------------------------------------------------------------------
/tweeter/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/python-sample-tweeterapp/b61b19dd30d5320253aa1fa28f7a2fb515c1124e/tweeter/__init__.py
--------------------------------------------------------------------------------
/tweeter/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/tweeter/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TweeterConfig(AppConfig):
5 | name = 'tweeter'
6 |
--------------------------------------------------------------------------------
/tweeter/fixtures/initial_data.json:
--------------------------------------------------------------------------------
1 | [
2 |
3 | {
4 | "pk": 1,
5 | "model": "auth.user",
6 | "fields": {
7 | "username": "nina",
8 | "first_name": "",
9 | "last_name": "",
10 | "is_active": true,
11 | "is_superuser": true,
12 | "is_staff": true,
13 | "last_login": "2014-08-31T00:20:41.771Z",
14 | "groups": [],
15 | "user_permissions": [],
16 | "password": "pbkdf2_sha256$12000$2LSBfxYO9fJJ$UT/BLyRLwBQIOUtOfA2aKkGw+xe44ZNYD2TWXqXoT3E=",
17 | "email": "",
18 | "date_joined": "2014-08-30T18:10:53.539Z"
19 | }
20 | },
21 | {
22 | "pk": 2,
23 | "model": "auth.user",
24 | "fields": {
25 | "username": "admin",
26 | "first_name": "Admin",
27 | "last_name": "",
28 | "is_active": true,
29 | "is_superuser": true,
30 | "is_staff": true,
31 | "last_login": "2014-08-31T00:22:38Z",
32 | "groups": [],
33 | "user_permissions": [],
34 | "password": "pbkdf2_sha256$12000$6JY3bvlplRf0$Rm6rcK9M3LNuTw1uZ3B/Je7rq420UCaf2iwmY8pIv2U=",
35 | "email": "admin@admin.com",
36 | "date_joined": "2014-08-31T00:22:38Z"
37 | }
38 | },
39 | {
40 | "pk": 3,
41 | "model": "auth.user",
42 | "fields": {
43 | "username": "bob",
44 | "first_name": "Bob",
45 | "last_name": "Bobman",
46 | "is_active": true,
47 | "is_superuser": false,
48 | "is_staff": false,
49 | "last_login": "2014-08-31T00:23:06Z",
50 | "groups": [],
51 | "user_permissions": [],
52 | "password": "pbkdf2_sha256$12000$ehuPT7BoKVpF$VZTxDeaHtLG7jU4wQ1erlFciwMk7l8aCp9MIjSVa/NU=",
53 | "email": "bob@bob.com",
54 | "date_joined": "2014-08-31T00:23:06Z"
55 | }
56 | },
57 | {
58 | "pk": 4,
59 | "model": "auth.user",
60 | "fields": {
61 | "username": "amy",
62 | "first_name": "Amy",
63 | "last_name": "Smith",
64 | "is_active": true,
65 | "is_superuser": false,
66 | "is_staff": false,
67 | "last_login": "2014-08-31T00:23:24Z",
68 | "groups": [],
69 | "user_permissions": [],
70 | "password": "pbkdf2_sha256$12000$Tv4vWeWICkfS$qcMP+xddceQZMVjHbdOhOsV6LKOQntKOkiaqdEzS2p8=",
71 | "email": "amy@smith.com",
72 | "date_joined": "2014-08-31T00:23:24Z"
73 | }
74 | },
75 | {
76 | "pk": 1,
77 | "model": "tweeter.tweet",
78 | "fields": {
79 | "text": "I'm an Admin! ",
80 | "user": 2,
81 | "timestamp": "2014-08-30T18:51:04Z"
82 | }
83 | },
84 | {
85 | "pk": 2,
86 | "model": "tweeter.tweet",
87 | "fields": {
88 | "text": "Bob is the coolest name!!",
89 | "user": 3,
90 | "timestamp": "2014-08-29T18:51:19Z"
91 | }
92 | },
93 | {
94 | "pk": 3,
95 | "model": "tweeter.tweet",
96 | "fields": {
97 | "text": "I <3 Tweeter",
98 | "user": 4,
99 | "timestamp": "2014-08-30T15:52:09Z"
100 | }
101 | },
102 | {
103 | "pk": 1,
104 | "model": "admin.logentry",
105 | "fields": {
106 | "action_flag": 1,
107 | "action_time": "2014-08-31T00:22:38.417Z",
108 | "object_repr": "Admin",
109 | "object_id": "2",
110 | "change_message": "",
111 | "user": 1,
112 | "content_type": 4
113 | }
114 | },
115 | {
116 | "pk": 2,
117 | "model": "admin.logentry",
118 | "fields": {
119 | "action_flag": 2,
120 | "action_time": "2014-08-31T00:22:57.048Z",
121 | "object_repr": "Admin",
122 | "object_id": "2",
123 | "change_message": "Changed first_name, email, is_staff and is_superuser.",
124 | "user": 1,
125 | "content_type": 4
126 | }
127 | },
128 | {
129 | "pk": 3,
130 | "model": "admin.logentry",
131 | "fields": {
132 | "action_flag": 1,
133 | "action_time": "2014-08-31T00:23:06.858Z",
134 | "object_repr": "Bob",
135 | "object_id": "3",
136 | "change_message": "",
137 | "user": 1,
138 | "content_type": 4
139 | }
140 | },
141 | {
142 | "pk": 4,
143 | "model": "admin.logentry",
144 | "fields": {
145 | "action_flag": 2,
146 | "action_time": "2014-08-31T00:23:17.098Z",
147 | "object_repr": "Bob",
148 | "object_id": "3",
149 | "change_message": "Changed first_name, last_name and email.",
150 | "user": 1,
151 | "content_type": 4
152 | }
153 | },
154 | {
155 | "pk": 5,
156 | "model": "admin.logentry",
157 | "fields": {
158 | "action_flag": 1,
159 | "action_time": "2014-08-31T00:23:24.481Z",
160 | "object_repr": "Amy",
161 | "object_id": "4",
162 | "change_message": "",
163 | "user": 1,
164 | "content_type": 4
165 | }
166 | },
167 | {
168 | "pk": 6,
169 | "model": "admin.logentry",
170 | "fields": {
171 | "action_flag": 2,
172 | "action_time": "2014-08-31T00:23:35.860Z",
173 | "object_repr": "Amy",
174 | "object_id": "4",
175 | "change_message": "Changed first_name, last_name and email.",
176 | "user": 1,
177 | "content_type": 4
178 | }
179 | },
180 | {
181 | "pk": 7,
182 | "model": "admin.logentry",
183 | "fields": {
184 | "action_flag": 1,
185 | "action_time": "2014-08-31T00:51:05.451Z",
186 | "object_repr": "I'm an Admin! ",
187 | "object_id": "1",
188 | "change_message": "",
189 | "user": 1,
190 | "content_type": 7
191 | }
192 | },
193 | {
194 | "pk": 8,
195 | "model": "admin.logentry",
196 | "fields": {
197 | "action_flag": 1,
198 | "action_time": "2014-08-31T00:51:26.097Z",
199 | "object_repr": "Bob is the coolest name EVAR",
200 | "object_id": "2",
201 | "change_message": "",
202 | "user": 1,
203 | "content_type": 7
204 | }
205 | },
206 | {
207 | "pk": 9,
208 | "model": "admin.logentry",
209 | "fields": {
210 | "action_flag": 1,
211 | "action_time": "2014-08-31T00:52:12.916Z",
212 | "object_repr": "I <3 Tweeter",
213 | "object_id": "3",
214 | "change_message": "",
215 | "user": 1,
216 | "content_type": 7
217 | }
218 | },
219 | {
220 | "pk": 10,
221 | "model": "admin.logentry",
222 | "fields": {
223 | "action_flag": 2,
224 | "action_time": "2014-08-31T01:06:18.846Z",
225 | "object_repr": "admin",
226 | "object_id": "2",
227 | "change_message": "Changed username.",
228 | "user": 1,
229 | "content_type": 4
230 | }
231 | },
232 | {
233 | "pk": 11,
234 | "model": "admin.logentry",
235 | "fields": {
236 | "action_flag": 2,
237 | "action_time": "2014-08-31T01:06:26.076Z",
238 | "object_repr": "bob",
239 | "object_id": "3",
240 | "change_message": "Changed username.",
241 | "user": 1,
242 | "content_type": 4
243 | }
244 | },
245 | {
246 | "pk": 12,
247 | "model": "admin.logentry",
248 | "fields": {
249 | "action_flag": 2,
250 | "action_time": "2014-08-31T01:06:34.096Z",
251 | "object_repr": "amy",
252 | "object_id": "4",
253 | "change_message": "Changed username.",
254 | "user": 1,
255 | "content_type": 4
256 | }
257 | }
258 | ]
--------------------------------------------------------------------------------
/tweeter/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.2 on 2018-10-10 06:35
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Tweet',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('text', models.CharField(max_length=140)),
22 | ('timestamp', models.DateTimeField(auto_now_add=True)),
23 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
24 | ],
25 | options={
26 | 'ordering': ['-timestamp'],
27 | },
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/tweeter/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoft/python-sample-tweeterapp/b61b19dd30d5320253aa1fa28f7a2fb515c1124e/tweeter/migrations/__init__.py
--------------------------------------------------------------------------------
/tweeter/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.db import models
3 |
4 |
5 | class Tweet(models.Model):
6 | user = models.ForeignKey(User, on_delete=models.CASCADE)
7 | text = models.CharField(max_length=140)
8 | timestamp = models.DateTimeField(auto_now_add=True)
9 |
10 | class Meta:
11 | ordering = ['-timestamp']
12 |
--------------------------------------------------------------------------------
/tweeter/permissions.py:
--------------------------------------------------------------------------------
1 | from rest_framework import permissions
2 |
3 |
4 | class IsSelfOrAdmin(permissions.BasePermission):
5 | """
6 | Object-level permission to only only the current User,
7 | or an admin User, to view and edit a User.
8 | """
9 |
10 | def has_object_permission(self, request, view, obj):
11 | # A User can edit and view their own data
12 | is_self = obj == request.user
13 | is_admin = request.user.is_superuser
14 | return is_self or is_admin
15 |
16 |
17 | class IsAuthorOrReadOnly(permissions.BasePermission):
18 | """
19 | Permission that allows only the author to edit
20 | tweets attributed to them
21 | """
22 | def has_object_permission(self, request, view, obj):
23 | if request.method in permissions.SAFE_METHODS:
24 | # Allow read only permissions to any user
25 | # to view the tweet
26 | return True
27 | else:
28 | # Check that the request user owns the object
29 | # being edited
30 | return obj.user == request.user
31 |
--------------------------------------------------------------------------------
/tweeter/serializers.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from rest_framework import serializers
3 |
4 | from tweeter.models import Tweet
5 |
6 |
7 | class TweetSerializer(serializers.ModelSerializer):
8 | user = serializers.SlugRelatedField(
9 | many=False,
10 | read_only=True,
11 | slug_field='username'
12 | )
13 |
14 | class Meta:
15 | model = Tweet
16 | fields = ('id', 'user', 'text', 'timestamp')
17 |
18 | def validate_text(self, value):
19 | if len(value) < 5:
20 | raise serializers.ValidationError(
21 | 'Text is too short.'
22 | )
23 | if len(value) > 140:
24 | raise serializers.ValidationError(
25 | 'Text is too long.'
26 | )
27 | return value
28 |
29 |
30 | class UserSerializer(serializers.ModelSerializer):
31 | tweets = TweetSerializer(many=True, source="tweet_set")
32 |
33 | class Meta:
34 | model = User
35 | fields = ('id','username', 'first_name', 'last_name', 'last_login', 'tweets')
36 |
--------------------------------------------------------------------------------
/tweeter/src/components/App.css:
--------------------------------------------------------------------------------
1 | @import-normalize;
2 |
3 | .main-body {
4 | display: flex;
5 | flex-direction: column;
6 | background-color: #11171d;
7 | min-height: 80rem;
8 | }
9 |
10 | body {
11 | display: flex;
12 | font-family: 'Helvetica';
13 | flex-direction: column;
14 | margin: 0;
15 | padding: 0;
16 | color:rgb(47, 47, 47);
17 | font-size: 14px;
18 | background-color: #ffffff;
19 | }
20 |
21 | #inner-body {
22 | margin: 16px
23 | }
24 |
25 | #menubar {
26 | display: flex;
27 | justify-content: space-between;
28 | align-items: center
29 | }
30 |
31 | .header {
32 | background-color: #3498db;
33 | padding: 1em 0;
34 | color: white;
35 | text-align: center;
36 | margin-top: 0;
37 | font-family: "Lato";
38 | font-size: 1.6em;
39 | }
40 |
41 | .icon-bar {
42 | color: blue;
43 | margin: 8;
44 | display: flex;
45 | width: 80px;
46 | justify-content: space-between
47 | }
48 |
49 | .icon-bar .fa {
50 | color: rgb(0, 120, 212)
51 | }
52 |
53 | .icon-bar .fa:hover {
54 | color: rgb(36, 58, 94)
55 | }
--------------------------------------------------------------------------------
/tweeter/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ReactDOM from "react-dom"
3 | import { BrowserRouter as Router, Route, Link } from "react-router-dom"
4 |
5 | import DataProvider from "./DataProvider"
6 | import Tweets from "./Tweets"
7 | import "./App.css"
8 | import {Login, Signup} from './Auth'
9 |
10 | class App extends Component {
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | addTweetText: '',
15 | tweets : []
16 | }
17 | }
18 |
19 | render() {
20 | return (
21 | Tweeter!
23 |
{placeholder}
; 40 | } 41 | } 42 | export default DataProvider; -------------------------------------------------------------------------------- /tweeter/src/components/Tweets.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .tweettext-container { 6 | display: flex; 7 | align-items: center 8 | } 9 | .tweettext-container button { 10 | height: 32px; 11 | } 12 | .tweettext-container input { 13 | width: 300px; 14 | } 15 | .tweet { 16 | display: flex; 17 | margin: 16px; 18 | align-items: center 19 | } 20 | 21 | .username { 22 | font-size: 1.2em 23 | } 24 | 25 | .bubble { 26 | width: 100%; 27 | background-color: #3d474b25; 28 | padding-left: 20px; 29 | position: relative; 30 | border-radius: 8px; 31 | text-align: left; 32 | display: inline-block; 33 | } 34 | .bubble:hover > .over-bubble { 35 | opacity: 1; 36 | } 37 | 38 | .bubble-container { 39 | width: 75%; 40 | display: block; 41 | position: relative; 42 | padding-left: 20px; 43 | display: inline-block; 44 | } 45 | 46 | .arrow { 47 | content: ''; 48 | display: block; 49 | position: absolute; 50 | left: 6px; 51 | bottom: 25%; 52 | height: 0; 53 | width: 0; 54 | border-top: 20px solid transparent; 55 | border-bottom: 20px solid transparent; 56 | border-right: 20px solid #ecf0f1; 57 | } 58 | 59 | .avatar { 60 | width: 64px; 61 | height: 64px; 62 | position: relative; 63 | overflow: hidden; 64 | } 65 | .avatar .fa:hover { 66 | color: rgb(115, 115, 115) 67 | } 68 | 69 | 70 | .icon-twitter { 71 | font-size: 1.5em; 72 | } 73 | 74 | .new { 75 | position: absolute; 76 | right: 5%; 77 | } 78 | 79 | .over-bubble { 80 | line-height: 1.4em; 81 | background-color: rgba(236, 240, 241, 0.8); 82 | position: relative; 83 | border-radius: 8px; 84 | text-align: center; 85 | display: inline-block; 86 | position: absolute !important; 87 | height: 100%; 88 | width: 100%; 89 | opacity: 0; 90 | top: 0; 91 | left: 0; 92 | z-index: 999; 93 | transition-property: all; 94 | transition-duration: 0.3s; 95 | transition-timing-function: ease-in; 96 | font-size: 2.8em; 97 | text-shadow: white 1px 1px 0; 98 | } 99 | 100 | .action-buttons { 101 | display: flex; 102 | height: 100%; 103 | width: 100%; 104 | justify-content: flex-end; 105 | align-items: flex-end 106 | } 107 | 108 | .action-buttons .fa:hover { 109 | color: rgb(0, 120, 212) 110 | } -------------------------------------------------------------------------------- /tweeter/src/components/Tweets.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from "prop-types"; 3 | import "./Tweets.css" 4 | import Cookies from 'js-cookie' 5 | 6 | class Sentiment extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | sentiment: null 11 | } 12 | } 13 | 14 | componentDidMount() { 15 | this.getSentiment() 16 | } 17 | 18 | async getSentiment() { 19 | const response = await fetch('http://msbuildsentiment.azurewebsites.net/' + this.props.message); 20 | const text = await response.text() 21 | this.setState({sentiment: text}) 22 | } 23 | 24 | render() { 25 | if (this.state.sentiment == null) { 26 | return 27 | } 28 | return (Nothing to show
} 79 | {this.state.tweets.map(tweet => ( 80 |@{tweet.user}
87 |{tweet.text}
88 |