diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..14700aff --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +FROM mcr.microsoft.com/vscode/devcontainers/universal:1-linux + +USER root + +# Add LDAP and python dependency build deps +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \ + apt-get -yq --no-install-recommends install gcc curl libsasl2-dev libldap2-dev libssl-dev python3-dev + +USER codespace diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..ef33dc3b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,58 @@ +// Update the VARIANT arg in docker-compose.yml to pick a Python version: 3, 3.8, 3.7, 3.6 +{ + "name": "Packet Codespace (python and postgres)", + "dockerComposeFile": "docker-compose.yaml", + "service": "app", + + // Set *default* container specific settings.json values on container create. + "settings": { + "sqltools.connections": [{ + "name": "Container database", + "driver": "PostgreSQL", + "previewLimit": 50, + "server": "localhost", + "port": 5432, + "database": "postgres", + "username": "postgres", + "password": "mysecretpassword" + }], + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": "/opt/python/latest/bin/python", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", + "python.testing.pytestPath": "/usr/local/py-utils/bin/pytest" + }, + "remoteUser": "codespace", + "overrideCommand": false, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/codespace/workspace,type=bind,consistency=cached", + "workspaceFolder": "/home/codespace/workspace", + "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined", "--privileged", "--init" ], + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "GitHub.vscode-pull-request-github", + "ms-python.python", + "mtxr.sqltools", + "mtxr.sqltools-driver-pg" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5000, 5432], + + // Use 'postCreateCommand' to run commands after the container is created. + // "oryx build" will automatically install your dependencies and attempt to build your project + "postCreateCommand": [ + "pip install --progress-bar=off install -r requirements.txt;", + "yarn install && `yarn bin gulp production`;", + "/home/codespace/.local/bin/flask db upgrade;" + ] +} diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml new file mode 100644 index 00000000..974d59f2 --- /dev/null +++ b/.devcontainer/docker-compose.yaml @@ -0,0 +1,41 @@ +version: '3' + +services: + app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + args: + NODE_VERSION: "10" + + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ..:/workspace:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:db + + # Uncomment the next line to use a non-root user for all processes. + user: codespace + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + db: + image: postgres:latest + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: postgres + POSTGRES_DB: postgres + POSTGRES_PASSWORD: mysecretpassword + + # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward MongoDB locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + +volumes: + postgres-data: diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 71dcdd5c..763c63e0 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -15,11 +15,11 @@ jobs: strategy: matrix: - python-version: [3.7, 3.8] + python-version: [3.9] steps: - name: Install ldap dependencies - run: sudo apt-get install libldap2-dev libsasl2-dev + run: sudo apt-get update && sudo apt-get install libldap2-dev libsasl2-dev - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 diff --git a/.pylintrc b/.pylintrc index a250c48e..bd778b58 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,7 @@ [MASTER] ignore = ,input persistent = yes +load-plugins = pylint_quotes [MESSAGES CONTROL] disable = diff --git a/Dockerfile b/Dockerfile index 98ca1d5e..2ca5882c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,10 @@ -FROM python:3.7-slim-buster +FROM python:3.9-slim-buster MAINTAINER Devin Matte +ENV DD_LOGS_INJECTION=true + RUN apt-get -yq update && \ - apt-get -yq --no-install-recommends install gcc curl libsasl2-dev libldap2-dev libssl-dev gnupg2 && \ + apt-get -yq --no-install-recommends install gcc curl libsasl2-dev libldap2-dev libssl-dev gnupg2 git && \ apt-get -yq clean all RUN mkdir /opt/packet @@ -30,4 +32,7 @@ RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && \ RUN ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime -CMD ["gunicorn", "packet:app", "--bind=0.0.0.0:8080", "--access-logfile=-", "--timeout=600"] +# Set version for apm +RUN echo "export DD_VERSION=$(python3 packet/git.py)" >> /tmp/version + +CMD ["/bin/bash", "-c", "source /tmp/version && ddtrace-run gunicorn packet:app --bind=0.0.0.0:8080 --access-logfile=- --timeout=600"] diff --git a/README.md b/README.md index d9d4271f..72355f36 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CSH Web Packet -[![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/) +[![Python 3.9](https://img.shields.io/badge/python-3.9-blue.svg)](https://www.python.org/downloads/release/python-390/) [![Build Status](https://travis-ci.com/ComputerScienceHouse/packet.svg?branch=develop)](https://travis-ci.com/ComputerScienceHouse/packet) Packet is used by CSH to facilitate the freshmen packet portion of our introductory member evaluation process. This is @@ -8,7 +8,7 @@ the second major iteration of packet on the web. The first version was [Tal packet](https://github.com/TalCohen/CSHWebPacket). ## Setup -**Requires Python 3.7 or newer.** +**Requires Python 3.9 or newer.** To get the server working you'll just need the Python dependencies and some secrets. There will be some UI issues due to missing assets though. To solve that you'll want to set up the front end dependencies or download a copy of the @@ -115,12 +115,12 @@ All DB commands are from the `Flask-Migrate` library and are used to configure D docs [here](https://flask-migrate.readthedocs.io/en/latest/) for details. ## Code standards -This project is configured to use Pylint. Commits will be pylinted by Travis CI and if the score drops your build will +This project is configured to use Pylint. Commits will be pylinted by GitHub actions and if the score drops your build will fail blocking you from merging. To make your life easier just run it before making a PR. To run pylint use this command: ```bash -pylint --load-plugins pylint_quotes packet/routes packet +pylint packet/routes packet ``` All python files should have a top-level docstring explaining the contents of the file and complex functions should diff --git a/config.env.py b/config.env.py index b14e3eb2..7e7b8832 100644 --- a/config.env.py +++ b/config.env.py @@ -15,7 +15,6 @@ # Logging config LOG_LEVEL = environ.get("PACKET_LOG_LEVEL", "INFO") -ANALYTICS_ID = environ.get("ANALYTICS_ID", "UA-420696-9") # OpenID Connect SSO config REALM = environ.get("PACKET_REALM", "csh") @@ -68,3 +67,8 @@ # Packet Config PACKET_UPPER = environ.get("PACKET_UPPER", "packet.csh.rit.edu") PACKET_INTRO = environ.get("PACKET_INTRO", "freshmen-packet.csh.rit.edu") + +# RUM +RUM_APP_ID = environ.get("PACKET_RUM_APP_ID", "") +RUM_CLIENT_TOKEN = environ.get("PACKET_RUM_CLIENT_TOKEN","") +DD_ENV = environ.get("DD_ENV", "local-dev") diff --git a/package.json b/package.json index 17a3c6c2..1b92a417 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "title": "CSH Packet", "name": "csh-packet", - "version": "3.5.2", + "version": "3.5.3", "description": "A web app implementation of the CSH introductory packet.", "bugs": { "url": "https://github.com/ComputerScienceHouse/packet/issues", diff --git a/packet/__init__.py b/packet/__init__.py index 89b7728b..c7fed4bc 100644 --- a/packet/__init__.py +++ b/packet/__init__.py @@ -19,6 +19,8 @@ from sentry_sdk.integrations.flask import FlaskIntegration from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration +from .git import get_version + app = Flask(__name__) gzip = Gzip(app) @@ -31,9 +33,8 @@ if os.path.exists(_pyfile_config): app.config.from_pyfile(_pyfile_config) -# Fetch the version number from the npm package file -with open(os.path.join(_root_dir, 'package.json')) as package_file: - app.config['VERSION'] = json.load(package_file)['version'] +# Fetch the version number +app.config['VERSION'] = get_version() # Logger configuration logging.getLogger().setLevel(app.config['LOG_LEVEL']) diff --git a/packet/git.py b/packet/git.py new file mode 100644 index 00000000..00e4d65f --- /dev/null +++ b/packet/git.py @@ -0,0 +1,49 @@ +import json +import os +import subprocess + +def get_short_sha(commit_ish: str = 'HEAD'): + """ + Get the short hash of a commit-ish + Returns '' if unfound + """ + + try: + rev_parse = subprocess.run(f'git rev-parse --short {commit_ish}'.split(), capture_output=True, check=True) + return rev_parse.stdout.decode('utf-8').strip() + except subprocess.CalledProcessError: + return '' + +def get_tag(commit_ish: str = 'HEAD'): + """ + Get the name of the tag at a given commit-ish + Returns '' if untagged + """ + + try: + describe = subprocess.run(f'git describe --exact-match {commit_ish}'.split(), capture_output=True, check=True) + return describe.stdout.decode('utf-8').strip() + except subprocess.CalledProcessError: + return '' + +def get_version(commit_ish: str = 'HEAD'): + """ + Get the version string of a commit-ish + + If we have a commit and the commit is tagged, version is `tag (commit-sha)` + If we have a commit but not a tag, version is `commit-sha` + If we have neither, version is the version field of package.json + """ + + if sha := get_short_sha(commit_ish): + if tag := get_tag(commit_ish): + return f'{tag} ({sha})' + else: + return sha + else: + root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + with open(os.path.join(root_dir, 'package.json')) as package_file: + return json.load(package_file)['version'] + +if __name__ == '__main__': + print(get_version()) diff --git a/packet/ldap.py b/packet/ldap.py index 4fcb7387..99b0367d 100644 --- a/packet/ldap.py +++ b/packet/ldap.py @@ -67,6 +67,24 @@ def _is_member_of_group(self, member, group): else: return group in member.groups + def get_groups(self, member): + if self.ldap: + return list( + map( + lambda g: g[0][3:], + filter( + lambda d: d[1] == 'cn=groups', + map( + lambda group_dn: group_dn.split(','), + member.get('memberOf') + ) + ) + ) + ) + else: + return member.groups + + # Getters diff --git a/packet/routes/upperclassmen.py b/packet/routes/upperclassmen.py index 11bc2b1f..92d5564d 100644 --- a/packet/routes/upperclassmen.py +++ b/packet/routes/upperclassmen.py @@ -3,12 +3,11 @@ """ import json -from itertools import chain from operator import itemgetter from flask import redirect, render_template, url_for from packet import app -from packet.models import Packet, MiscSignature +from packet.models import Packet from packet.utils import before_request, packet_auth from packet.log_utils import log_cache, log_time from packet.stats import packet_stats @@ -51,18 +50,20 @@ def upperclassmen_total(info=None): # Sum up the signed packets per upperclassman upperclassmen = dict() + misc = dict() for packet in open_packets: - for sig in chain(packet.upper_signatures, packet.misc_signatures): + for sig in packet.upper_signatures: if sig.member not in upperclassmen: upperclassmen[sig.member] = 0 - if isinstance(sig, MiscSignature): - upperclassmen[sig.member] += 1 - elif sig.signed: - upperclassmen[sig.member] += 1 + if sig.signed: + upperclassmen[sig.member] += 1 + for sig in packet.misc_signatures: + misc[sig.member] = 1 + misc.get(sig.member, 0) return render_template('upperclassmen_totals.html', info=info, num_open_packets=len(open_packets), - upperclassmen=sorted(upperclassmen.items(), key=itemgetter(1), reverse=True)) + upperclassmen=sorted(upperclassmen.items(), key=itemgetter(1), reverse=True), + misc=sorted(misc.items(), key=itemgetter(1), reverse=True)) @app.route('/stats/packet/') diff --git a/packet/templates/include/head.html b/packet/templates/include/head.html index 12ff14c1..20d0f420 100644 --- a/packet/templates/include/head.html +++ b/packet/templates/include/head.html @@ -34,6 +34,35 @@ + + + + + - - - - diff --git a/packet/templates/upperclassmen_totals.html b/packet/templates/upperclassmen_totals.html index 814062e5..2b56955c 100644 --- a/packet/templates/upperclassmen_totals.html +++ b/packet/templates/upperclassmen_totals.html @@ -11,7 +11,7 @@

Upperclassmen Signatures

{% if num_open_packets > 0 %}
-
+
Upperclassmen Signatures data-length-changable="true" data-paginated="false"> - + @@ -45,6 +45,40 @@

Upperclassmen Signatures

+
+
+
+
UpperclassmanActive Member Signatures
+ + + + + + + + {% for member, signed_count in misc %} + + + + + {% endfor %} + +
Alumni or AdvisorSignatures
+ + {{ member }} {{ get_csh_name(member) }} + + + {{ signed_count }}/{{ num_open_packets }} +
+
+
+
{% else %}