aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Meinhold <mxmeinhold@gmail.com>2021-01-26 12:27:14 -0500
committerGitHub <noreply@github.com>2021-01-26 12:27:14 -0500
commite4d19a78c6bc1c029d306ebb0276f821cdbd424c (patch)
tree8d2d4ed578b87e899719644e2dc0c7414dc5b9a3
parent5e09efe83ee3656efdaac9155fffa351ef403175 (diff)
parent9a706d2bccb32ae7bd280daaae15d24dfa0c1df2 (diff)
Merge pull request #236 from ComputerScienceHouse/developv3.5.2
-rw-r--r--.github/dependabot.yml16
-rw-r--r--.github/workflows/docker-build.yml32
-rw-r--r--.github/workflows/node-js.yml27
-rw-r--r--.github/workflows/python-app.yml34
-rw-r--r--.travis.yml16
-rw-r--r--Dockerfile2
-rw-r--r--README.md14
-rw-r--r--config.env.py17
-rw-r--r--package.json6
-rw-r--r--packet/__init__.py34
-rw-r--r--packet/context_processors.py5
-rw-r--r--packet/ldap.py413
-rw-r--r--packet/log_utils.py5
-rw-r--r--packet/notifications.py19
-rw-r--r--packet/routes/api.py9
-rw-r--r--packet/templates/packet.html2
-rw-r--r--packet/utils.py52
-rw-r--r--requirements.txt16
-rw-r--r--yarn.lock6
19 files changed, 469 insertions, 256 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..94a31c7
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,16 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "npm" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
+
+ - package-ecosystem: "pip" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
new file mode 100644
index 0000000..d929520
--- /dev/null
+++ b/.github/workflows/docker-build.yml
@@ -0,0 +1,32 @@
+name: Docker
+
+on:
+ push:
+ branches:
+ - develop
+ - master
+
+ # Run tests for any PRs.
+ pull_request:
+ branches:
+ - develop
+ - master
+
+env:
+ IMAGE_NAME: packet
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Run tests
+ run: |
+ if [ -f docker-compose.test.yml ]; then
+ docker-compose --file docker-compose.test.yml build
+ docker-compose --file docker-compose.test.yml run sut
+ else
+ docker build . --file Dockerfile
+ fi
diff --git a/.github/workflows/node-js.yml b/.github/workflows/node-js.yml
new file mode 100644
index 0000000..c2f0af6
--- /dev/null
+++ b/.github/workflows/node-js.yml
@@ -0,0 +1,27 @@
+# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
+
+name: Node.js CI
+
+on:
+ push:
+ branches: [develop, master]
+ pull_request:
+ branches: [develop, master]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [10.x, 12.x, 14.x]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: yarn install
+ - run: yarn prod
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
new file mode 100644
index 0000000..71dcdd5
--- /dev/null
+++ b/.github/workflows/python-app.yml
@@ -0,0 +1,34 @@
+# This workflow will install Python dependencies, run tests and lint with a single version of Python
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Python application
+
+on:
+ push:
+ branches: [master, develop]
+ pull_request:
+ branches: [master, develop]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ python-version: [3.7, 3.8]
+
+ steps:
+ - name: Install ldap dependencies
+ run: sudo apt-get install libldap2-dev libsasl2-dev
+ - uses: actions/checkout@v2
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+ - name: Lint with pylint
+ run: |
+ pylint packet
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index d0cbb08..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-language: python
-python:
- - "3.7"
-services:
- - "docker"
-
-install:
- - "pip install -r requirements.txt"
- - "curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.35.3/install.sh | bash"
- - "nvm install"
- - "nvm use"
- - "npm install -g gulp"
- - "npm install"
-script:
- - "gulp lint"
- - "docker build -t packet ."
diff --git a/Dockerfile b/Dockerfile
index 594379e..98ca1d5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@ FROM python:3.7-slim-buster
MAINTAINER Devin Matte <matted@csh.rit.edu>
RUN apt-get -yq update && \
- apt-get -yq --no-install-recommends install gcc curl libsasl2-dev libldap2-dev libssl-dev && \
+ apt-get -yq --no-install-recommends install gcc curl libsasl2-dev libldap2-dev libssl-dev gnupg2 && \
apt-get -yq clean all
RUN mkdir /opt/packet
diff --git a/README.md b/README.md
index 06d4e1a..d9d4271 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,20 @@ If it doesn't work for some reason, you may have to globally install gulp throug
npm install -g gulp
```
+### Local Development
+* PostgreSQL
+You'll need a postgres instance to use as a development DB.
+You can use an existing database, like the instance used for the dev branch, use a database on another server, or spin up a container using docker or podman.
+To get setup using docker, run
+```bash
+docker run --name packet-postgres -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432:5432 postgres
+```
+After the container starts up, you should be able to connect with the connection string `postgresql://postgres:mysecretpassword@localhost:5432/postgres`, which is the default connection string in `config.env.py`.
+Once the container is up, run the following to set up the database tables.
+```bash
+flask db upgrade
+```
+
### Secrets and configuration
Packet supports 2 primary configuration methods:
1. Environment variables - See `config.env.py` for the expected names and default values.
diff --git a/config.env.py b/config.env.py
index 02125fa..b14e3eb 100644
--- a/config.env.py
+++ b/config.env.py
@@ -3,7 +3,7 @@ Default configuration settings and environment variable based configuration logi
See the readme for more information
"""
from distutils.util import strtobool
-from os import environ
+from os import environ, path, getcwd
# Flask config
DEBUG = False
@@ -25,12 +25,25 @@ OIDC_CLIENT_ID = environ.get("PACKET_OIDC_CLIENT_ID", "packet")
OIDC_CLIENT_SECRET = environ.get("PACKET_OIDC_CLIENT_SECRET", "PLEASE_REPLACE_ME")
# SQLAlchemy config
-SQLALCHEMY_DATABASE_URI = environ.get("PACKET_DATABASE_URI", None)
+SQLALCHEMY_DATABASE_URI = environ.get("PACKET_DATABASE_URI", "postgresql://postgres:mysecretpassword@localhost:5432/postgres")
SQLALCHEMY_TRACK_MODIFICATIONS = False
# LDAP config
LDAP_BIND_DN = environ.get("PACKET_LDAP_BIND_DN", None)
LDAP_BIND_PASS = environ.get("PACKET_LDAP_BIND_PASS", None)
+LDAP_MOCK_MEMBERS = [
+ {'uid':'evals', 'groups': ['eboard', 'eboard-evaluations', 'active']},
+ {'uid':'imps-3da', 'groups': ['eboard', 'eboard-imps', '3da', 'active']},
+ {
+ 'uid':'rtp-cm-webs-onfloor',
+ 'groups': ['active-rtp', 'rtp', 'constitutional_maintainers', 'webmaster', 'active', 'onfloor'],
+ 'room_number': 1024
+ },
+ {'uid':'misc-rtp', 'groups': ['rtp']},
+ {'uid':'onfloor', 'groups': ['active', 'onfloor'], 'room_number': 1024},
+ {'uid':'active-offfloor', 'groups': ['active']},
+ {'uid':'alum', 'groups': ['member']},
+ ]
# Mail Config
MAIL_PROD = strtobool(environ.get("PACKET_MAIL_PROD", "False"))
diff --git a/package.json b/package.json
index b97168d..17a3c6c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"title": "CSH Packet",
"name": "csh-packet",
- "version": "3.5.1",
+ "version": "3.5.2",
"description": "A web app implementation of the CSH introductory packet.",
"bugs": {
"url": "https://github.com/ComputerScienceHouse/packet/issues",
@@ -13,6 +13,10 @@
"Joel Eager (https://github.com/JoelEager)",
"Max Meinhold (https://github.com/mxmeinhold)"
],
+ "scripts": {
+ "prod": "gulp production",
+ "lint": "gulp lint"
+ },
"repository": {
"type": "git",
"url": "https://github.com/ComputerScienceHouse/packet.git"
diff --git a/packet/__init__.py b/packet/__init__.py
index 32e9fe7..89b7728 100644
--- a/packet/__init__.py
+++ b/packet/__init__.py
@@ -50,19 +50,31 @@ APP_CONFIG = ProviderConfiguration(issuer=app.config['OIDC_ISSUER'],
app.config['OIDC_CLIENT_SECRET']))
# Initialize Onesignal Notification apps
-csh_onesignal_client = onesignal.Client(user_auth_key=app.config['ONESIGNAL_USER_AUTH_KEY'],
- app_auth_key=app.config['ONESIGNAL_CSH_APP_AUTH_KEY'],
- app_id=app.config['ONESIGNAL_CSH_APP_ID'])
-
-intro_onesignal_client = onesignal.Client(user_auth_key=app.config['ONESIGNAL_USER_AUTH_KEY'],
- app_auth_key=app.config['ONESIGNAL_INTRO_APP_AUTH_KEY'],
- app_id=app.config['ONESIGNAL_INTRO_APP_ID'])
+csh_onesignal_client = None
+if app.config['ONESIGNAL_USER_AUTH_KEY'] and \
+ app.config['ONESIGNAL_CSH_APP_AUTH_KEY'] and \
+ app.config['ONESIGNAL_CSH_APP_ID']:
+ csh_onesignal_client = onesignal.Client(
+ user_auth_key=app.config['ONESIGNAL_USER_AUTH_KEY'],
+ app_auth_key=app.config['ONESIGNAL_CSH_APP_AUTH_KEY'],
+ app_id=app.config['ONESIGNAL_CSH_APP_ID']
+ )
+ app.logger.info('CSH Onesignal configured and notifications enabled')
+
+intro_onesignal_client = None
+if app.config['ONESIGNAL_USER_AUTH_KEY'] and \
+ app.config['ONESIGNAL_INTRO_APP_AUTH_KEY'] and \
+ app.config['ONESIGNAL_INTRO_APP_ID']:
+ intro_onesignal_client = onesignal.Client(
+ user_auth_key=app.config['ONESIGNAL_USER_AUTH_KEY'],
+ app_auth_key=app.config['ONESIGNAL_INTRO_APP_AUTH_KEY'],
+ app_id=app.config['ONESIGNAL_INTRO_APP_ID']
+ )
+ app.logger.info('Intro Onesignal configured and notifications enabled')
# OIDC Auth
auth = OIDCAuthentication({'app': APP_CONFIG}, app)
-
-# LDAP
-_ldap = csh_ldap.CSHLDAP(app.config['LDAP_BIND_DN'], app.config['LDAP_BIND_PASS'])
+app.logger.info('OIDCAuth configured')
# Sentry
sentry_sdk.init(
@@ -70,9 +82,9 @@ sentry_sdk.init(
integrations=[FlaskIntegration(), SqlalchemyIntegration()]
)
-app.logger.info('OIDCAuth and LDAP configured')
# pylint: disable=wrong-import-position
+from .ldap import ldap
from . import models
from . import context_processors
from . import commands
diff --git a/packet/context_processors.py b/packet/context_processors.py
index 2e8d5c5..bff75b1 100644
--- a/packet/context_processors.py
+++ b/packet/context_processors.py
@@ -6,16 +6,15 @@ import urllib
from functools import lru_cache
from datetime import datetime
-from packet.ldap import ldap_get_member
from packet.models import Freshman
-from packet import app
+from packet import app, ldap
# pylint: disable=bare-except
@lru_cache(maxsize=128)
def get_csh_name(username):
try:
- member = ldap_get_member(username)
+ member = ldap.get_member(username)
return member.cn + ' (' + member.uid + ')'
except:
return username
diff --git a/packet/ldap.py b/packet/ldap.py
index 0f78e17..4fcb738 100644
--- a/packet/ldap.py
+++ b/packet/ldap.py
@@ -5,197 +5,260 @@ Helper functions for working with the csh_ldap library
from functools import lru_cache
from datetime import date
-from packet import _ldap
+from csh_ldap import CSHLDAP
+from packet import app
-def _ldap_get_group_members(group):
- """
- :return: A list of CSHMember instances
- """
- return _ldap.get_group(group).get_members()
+class MockMember:
-def _ldap_is_member_of_group(member, group):
- """
- :param member: A CSHMember instance
- """
- for group_dn in member.get('memberOf'):
- if group == group_dn.split(',')[0][3:]:
- return True
+ def __init__(self, uid: str, groups: list = None, cn: str = None, room_number: int = None):
+ self.uid = uid
+ self.groups = groups if groups else list()
+ if room_number:
+ self.room_number = room_number
+ self.cn = cn if cn else uid.title() # pylint: disable=invalid-name
- return False
+ def __eq__(self, other):
+ if type(other) is type(self):
+ return self.uid == other.uid
+ return False
-# Getters
-@lru_cache(maxsize=256)
-def ldap_get_member(username):
- """
- :return: A CSHMember instance
- """
- return _ldap.get_member(username, uid=True)
+ def __hash__(self):
+ return hash(self.uid)
-def ldap_get_active_members():
- """
- Gets all current, dues-paying members
- :return: A list of CSHMember instances
- """
- return _ldap_get_group_members('active')
+ def __repr__(self):
+ return f'MockMember(uid: {self.uid}, groups: {self.groups})'
-def ldap_get_intro_members():
- """
- Gets all freshmen members
- :return: A list of CSHMember instances
- """
- return _ldap_get_group_members('intromembers')
-
-
-def ldap_get_eboard():
- """
- Gets all voting members of eboard
- :return: A list of CSHMember instances
- """
- members = _ldap_get_group_members('eboard-chairman') + _ldap_get_group_members('eboard-evaluations'
- ) + _ldap_get_group_members('eboard-financial') + _ldap_get_group_members('eboard-history'
- ) + _ldap_get_group_members('eboard-imps') + _ldap_get_group_members('eboard-opcomm'
- ) + _ldap_get_group_members('eboard-research') + _ldap_get_group_members('eboard-social'
- )
-
- return members
-
-
-def ldap_get_live_onfloor():
- """
- All upperclassmen who live on floor and are not eboard
- :return: A list of CSHMember instances
- """
- members = []
- onfloor = _ldap_get_group_members('onfloor')
- for member in onfloor:
- if ldap_get_roomnumber(member) and not ldap_is_eboard(member):
- members.append(member)
+class LDAPWrapper:
- return members
+ def __init__(self, cshldap=None, mock_members=None):
+ self.ldap = cshldap
+ self.mock_members = mock_members
+ if self.ldap:
+ app.logger.info('LDAP configured with CSH LDAP')
+ else:
+ app.logger.info('LDAP configured with local mock')
-def ldap_get_active_rtps():
- """
- All active RTPs
- :return: A list of CSHMember instances
- """
- return [member.uid for member in _ldap_get_group_members('active_rtp')]
+ def _get_group_members(self, group):
+ """
+ :return: A list of CSHMember instances
+ """
+ if self.ldap:
+ return self.ldap.get_group(group).get_members()
+ else:
+ return list(filter(lambda member: group in member.groups, self.mock_members))
-def ldap_get_3das():
- """
- All 3das
- :return: A list of CSHMember instances
- """
- return [member.uid for member in _ldap_get_group_members('3da')]
+ def _is_member_of_group(self, member, group):
+ """
+ :param member: A CSHMember instance
+ """
+ if self.ldap:
+ for group_dn in member.get('memberOf'):
+ if group == group_dn.split(',')[0][3:]:
+ return True
+ return False
+ else:
+ return group in member.groups
-def ldap_get_webmasters():
- """
- All webmasters
- :return: A list of CSHMember instances
- """
- return [member.uid for member in _ldap_get_group_members('webmaster')]
-
-
-def ldap_get_constitutional_maintainers():
- """
- All constitutional maintainers
- :return: A list of CSHMember instances
- """
- return [member.uid for member in _ldap_get_group_members('constitutional_maintainers')]
-
-
-def ldap_get_wiki_maintainers():
- """
- All wiki maintainers
- :return: A list of CSHMember instances
- """
- return [member.uid for member in _ldap_get_group_members('wiki_maintainers')]
-
-
-def ldap_get_drink_admins():
- """
- All drink admins
- :return: A list of CSHMember instances
- """
- return [member.uid for member in _ldap_get_group_members('drink')]
-
-
-def ldap_get_eboard_role(member):
- """
- :param member: A CSHMember instance
- :return: A String or None
- """
-
- return_val = None
-
- if _ldap_is_member_of_group(member, 'eboard-chairman'):
- return_val = 'Chairman'
- elif _ldap_is_member_of_group(member, 'eboard-evaluations'):
- return_val = 'Evals'
- elif _ldap_is_member_of_group(member, 'eboard-financial'):
- return_val = 'Financial'
- elif _ldap_is_member_of_group(member, 'eboard-history'):
- return_val = 'History'
- elif _ldap_is_member_of_group(member, 'eboard-imps'):
- return_val = 'Imps'
- elif _ldap_is_member_of_group(member, 'eboard-opcomm'):
- return_val = 'OpComm'
- elif _ldap_is_member_of_group(member, 'eboard-research'):
- return_val = 'R&D'
- elif _ldap_is_member_of_group(member, 'eboard-social'):
- return_val = 'Social'
- elif _ldap_is_member_of_group(member, 'eboard-secretary'):
- return_val = 'Secretary'
-
- return return_val
-
-
-# Status checkers
-def ldap_is_eboard(member):
- """
- :param member: A CSHMember instance
- """
- return _ldap_is_member_of_group(member, 'eboard')
-
-
-def ldap_is_evals(member):
- return _ldap_is_member_of_group(member, 'eboard-evaluations')
-
-
-def ldap_is_rtp(member):
- return _ldap_is_member_of_group(member, 'rtp')
-
-
-def ldap_is_intromember(member):
- """
- :param member: A CSHMember instance
- """
- return _ldap_is_member_of_group(member, 'intromembers')
-
-
-def ldap_is_on_coop(member):
- """
- :param member: A CSHMember instance
- """
- if date.today().month > 6:
- return _ldap_is_member_of_group(member, 'fall_coop')
- else:
- return _ldap_is_member_of_group(member, 'spring_coop')
-
-
-def ldap_get_roomnumber(member):
- """
- :param member: A CSHMember instance
- """
- try:
- return member.roomNumber
- except AttributeError:
- return None
+ # Getters
+
+ @lru_cache(maxsize=256)
+ def get_member(self, username):
+ """
+ :return: A CSHMember instance
+ """
+ if self.ldap:
+ return self.ldap.get_member(username, uid=True)
+ else:
+ member = next(filter(lambda member: member.uid == username, self.mock_members), None)
+ if member:
+ return member
+ raise KeyError('Invalid Search Name')
+
+
+ def get_active_members(self):
+ """
+ Gets all current, dues-paying members
+ :return: A list of CSHMember instances
+ """
+ return self._get_group_members('active')
+
+
+ def get_intro_members(self):
+ """
+ Gets all freshmen members
+ :return: A list of CSHMember instances
+ """
+ return self._get_group_members('intromembers')
+
+
+ def get_eboard(self):
+ """
+ Gets all voting members of eboard
+ :return: A list of CSHMember instances
+ """
+ members = self._get_group_members('eboard-chairman') + self._get_group_members('eboard-evaluations'
+ ) + self._get_group_members('eboard-financial') + self._get_group_members('eboard-history'
+ ) + self._get_group_members('eboard-imps') + self._get_group_members('eboard-opcomm'
+ ) + self._get_group_members('eboard-research') + self._get_group_members('eboard-social'
+ )
+
+ return members
+
+
+ def get_live_onfloor(self):
+ """
+ All upperclassmen who live on floor and are not eboard
+ :return: A list of CSHMember instances
+ """
+ members = []
+ onfloor = self._get_group_members('onfloor')
+ for member in onfloor:
+ if self.get_roomnumber(member) and not self.is_eboard(member):
+ members.append(member)
+
+ return members
+
+
+ def get_active_rtps(self):
+ """
+ All active RTPs
+ :return: A list of CSHMember instances
+ """
+ return [member.uid for member in self._get_group_members('active_rtp')]
+
+
+ def get_3das(self):
+ """
+ All 3das
+ :return: A list of CSHMember instances
+ """
+ return [member.uid for member in self._get_group_members('3da')]
+
+
+ def get_webmasters(self):
+ """
+ All webmasters
+ :return: A list of CSHMember instances
+ """
+ return [member.uid for member in self._get_group_members('webmaster')]
+
+
+ def get_constitutional_maintainers(self):
+ """
+ All constitutional maintainers
+ :return: A list of CSHMember instances
+ """
+ return [member.uid for member in self._get_group_members('constitutional_maintainers')]
+
+ def get_wiki_maintainers(self):
+ """
+ All wiki maintainers
+ :return: A list of CSHMember instances
+ """
+ return [member.uid for member in self._get_group_members('wiki_maintainers')]
+
+
+ def get_drink_admins(self):
+ """
+ All drink admins
+ :return: A list of CSHMember instances
+ """
+ return [member.uid for member in self._get_group_members('drink')]
+
+
+ def get_eboard_role(self, member):
+ """
+ :param member: A CSHMember instance
+ :return: A String or None
+ """
+
+ return_val = None
+
+ if self._is_member_of_group(member, 'eboard-chairman'):
+ return_val = 'Chairperson'
+ elif self._is_member_of_group(member, 'eboard-evaluations'):
+ return_val = 'Evals'
+ elif self._is_member_of_group(member, 'eboard-financial'):
+ return_val = 'Financial'
+ elif self._is_member_of_group(member, 'eboard-history'):
+ return_val = 'History'
+ elif self._is_member_of_group(member, 'eboard-imps'):
+ return_val = 'Imps'
+ elif self._is_member_of_group(member, 'eboard-opcomm'):
+ return_val = 'OpComm'
+ elif self._is_member_of_group(member, 'eboard-research'):
+ return_val = 'R&D'
+ elif self._is_member_of_group(member, 'eboard-social'):
+ return_val = 'Social'
+ elif self._is_member_of_group(member, 'eboard-secretary'):
+ return_val = 'Secretary'
+
+ return return_val
+
+
+ # Status checkers
+ def is_eboard(self, member):
+ """
+ :param member: A CSHMember instance
+ """
+ return self._is_member_of_group(member, 'eboard')
+
+
+ def is_evals(self, member):
+ return self._is_member_of_group(member, 'eboard-evaluations')
+
+
+ def is_rtp(self, member):
+ return self._is_member_of_group(member, 'rtp')
+
+
+ def is_intromember(self, member):
+ """
+ :param member: A CSHMember instance
+ """
+ return self._is_member_of_group(member, 'intromembers')
+
+
+ def is_on_coop(self, member):
+ """
+ :param member: A CSHMember instance
+ """
+ if date.today().month > 6:
+ return self._is_member_of_group(member, 'fall_coop')
+ else:
+ return self._is_member_of_group(member, 'spring_coop')
+
+
+ def get_roomnumber(self, member): # pylint: disable=no-self-use
+ """
+ :param member: A CSHMember instance
+ """
+ try:
+ return member.roomNumber
+ except AttributeError:
+ return None
+
+
+if app.config['LDAP_BIND_DN'] and app.config['LDAP_BIND_PASS']:
+ ldap = LDAPWrapper(cshldap=CSHLDAP(app.config['LDAP_BIND_DN'],
+ app.config['LDAP_BIND_PASS']
+ )
+)
+else:
+ ldap = LDAPWrapper(
+ mock_members=list(
+ map(
+ lambda mock_dict: MockMember(**mock_dict),
+ app.config['LDAP_MOCK_MEMBERS']
+ )
+ )
+ )
diff --git a/packet/log_utils.py b/packet/log_utils.py
index 66c1453..5481bef 100644
--- a/packet/log_utils.py
+++ b/packet/log_utils.py
@@ -5,9 +5,8 @@ General utilities for logging metadata
from functools import wraps
from datetime import datetime
-from packet import app
+from packet import app, ldap
from packet.context_processors import get_rit_name
-from packet.ldap import ldap_get_member
from packet.utils import is_freshman_on_floor
@@ -39,7 +38,7 @@ def _format_cache(func):
# Tuple of lru_cache functions to log stats from
-_caches = (get_rit_name, ldap_get_member, is_freshman_on_floor)
+_caches = (get_rit_name, ldap.get_member, is_freshman_on_floor)
def log_cache(func):
diff --git a/packet/notifications.py b/packet/notifications.py
index a3a93f2..e718265 100644
--- a/packet/notifications.py
+++ b/packet/notifications.py
@@ -11,6 +11,20 @@ post_body = {
'url': app.config['PROTOCOL'] + app.config['SERVER_NAME']
}
+def require_onesignal_intro(func):
+ def require_onesignal_intro_wrapper(*args, **kwargs):
+ if intro_onesignal_client:
+ return func(*args, **kwargs)
+ return None
+ return require_onesignal_intro_wrapper
+
+def require_onesignal_csh(func):
+ def require_onesignal_csh_wrapper(*args, **kwargs):
+ if csh_onesignal_client:
+ return func(*args, **kwargs)
+ return None
+ return require_onesignal_csh_wrapper
+
def send_notification(notification_body, subscriptions, client):
tokens = list(map(lambda subscription: subscription.token, subscriptions))
@@ -24,6 +38,7 @@ def send_notification(notification_body, subscriptions, client):
app.logger.warn('The notification ({}) was unsuccessful'.format(notification.post_body))
+@require_onesignal_intro
def packet_signed_notification(packet, signer):
subscriptions = NotificationSubscription.query.filter_by(freshman_username=packet.freshman_username)
if subscriptions:
@@ -36,6 +51,8 @@ def packet_signed_notification(packet, signer):
send_notification(notification_body, subscriptions, intro_onesignal_client)
+@require_onesignal_csh
+@require_onesignal_intro
def packet_100_percent_notification(packet):
member_subscriptions = NotificationSubscription.query.filter(NotificationSubscription.member.isnot(None))
intro_subscriptions = NotificationSubscription.query.filter(NotificationSubscription.freshman_username.isnot(None))
@@ -50,6 +67,7 @@ def packet_100_percent_notification(packet):
send_notification(notification_body, intro_subscriptions, intro_onesignal_client)
+@require_onesignal_intro
def packet_starting_notification(packet):
subscriptions = NotificationSubscription.query.filter_by(freshman_username=packet.freshman_username)
if subscriptions:
@@ -62,6 +80,7 @@ def packet_starting_notification(packet):
send_notification(notification_body, subscriptions, intro_onesignal_client)
+@require_onesignal_csh
def packets_starting_notification(start_date):
member_subscriptions = NotificationSubscription.query.filter(NotificationSubscription.member.isnot(None))
if member_subscriptions:
diff --git a/packet/routes/api.py b/packet/routes/api.py
index 828ac96..ee582ce 100644
--- a/packet/routes/api.py
+++ b/packet/routes/api.py
@@ -6,9 +6,8 @@ from json import dumps
from flask import session, request
-from packet import app, db
+from packet import app, db, ldap
from packet.context_processors import get_rit_name
-from packet.ldap import _ldap_is_member_of_group, ldap_get_member
from packet.log_utils import log_time
from packet.mail import send_report_mail
from packet.utils import before_request, packet_auth, notify_slack, sync_freshman as sync_freshman_list, \
@@ -42,7 +41,7 @@ def sync_freshman():
# Only allow evals to create new frosh
username = str(session['userinfo'].get('preferred_username', ''))
- if not _ldap_is_member_of_group(ldap_get_member(username), 'eboard-evaluations'):
+ if not ldap.is_evals(ldap.get_member(username)):
return 'Forbidden: not Evaluations Director', 403
freshmen_in_post = {freshman.rit_username: freshman for freshman in map(POSTFreshman, request.json)}
@@ -71,7 +70,7 @@ def create_packet():
# Only allow evals to create new packets
username = str(session['userinfo'].get('preferred_username', ''))
- if not _ldap_is_member_of_group(ldap_get_member(username), 'eboard-evaluations'):
+ if not ldap.is_evals(ldap.get_member(username)):
return 'Forbidden: not Evaluations Director', 403
base_date = datetime.strptime(request.json['start_date'], '%m/%d/%Y').date()
@@ -89,7 +88,7 @@ def create_packet():
def sync_ldap():
# Only allow evals to sync ldap
username = str(session['userinfo'].get('preferred_username', ''))
- if not _ldap_is_member_of_group(ldap_get_member(username), 'eboard-evaluations'):
+ if not ldap.is_evals(ldap.get_member(username)):
return 'Forbidden: not Evaluations Director', 403
sync_with_ldap()
return dumps('Done'), 201
diff --git a/packet/templates/packet.html b/packet/templates/packet.html
index 8e30376..c7ddd5a 100644
--- a/packet/templates/packet.html
+++ b/packet/templates/packet.html
@@ -1,6 +1,6 @@
{% extends "extend/base.html" %}
-{% set packet_end = packet.end.strftime('%m/%d/%Y') %}
+{% set packet_end = packet.end.strftime('%m/%d/%Y, %H:%M') %}
{% block body %}
<div class="container main">
diff --git a/packet/utils.py b/packet/utils.py
index 0f979f8..4caa673 100644
--- a/packet/utils.py
+++ b/packet/utils.py
@@ -7,12 +7,9 @@ from functools import wraps, lru_cache
import requests
from flask import session, redirect
-from packet import auth, app, db
+from packet import auth, app, db, ldap
from packet.mail import send_start_packet_mail
from packet.models import Freshman, FreshSignature, Packet, UpperSignature, MiscSignature
-from packet.ldap import ldap_get_member, ldap_is_intromember, ldap_is_evals, ldap_is_on_coop, \
- ldap_get_active_members, ldap_get_active_rtps, ldap_get_3das, ldap_get_wiki_maintainers, ldap_get_webmasters, \
- ldap_get_constitutional_maintainers, ldap_get_drink_admins, ldap_get_eboard_role
from packet.notifications import packets_starting_notification, packet_starting_notification
INTRO_REALM = 'https://sso.csh.rit.edu/auth/realms/intro'
@@ -35,11 +32,11 @@ def before_request(func):
'admin': False # It's always false if frosh
}
else:
- member = ldap_get_member(uid)
+ member = ldap.get_member(uid)
info = {
'realm': 'csh',
'uid': uid,
- 'admin': ldap_is_evals(member)
+ 'admin': ldap.is_evals(member)
}
kwargs['info'] = info
@@ -70,7 +67,7 @@ def packet_auth(func):
def wrapped_function(*args, **kwargs):
if app.config['REALM'] == 'csh':
username = str(session['userinfo'].get('preferred_username', ''))
- if ldap_is_intromember(ldap_get_member(username)):
+ if ldap.is_intromember(ldap.get_member(username)):
app.logger.warn('Stopped intro member {} from accessing upperclassmen packet'.format(username))
return redirect(app.config['PROTOCOL'] + app.config['PACKET_INTRO'], code=301)
@@ -89,8 +86,8 @@ def admin_auth(func):
def wrapped_function(*args, **kwargs):
if app.config['REALM'] == 'csh':
username = str(session['userinfo'].get('preferred_username', ''))
- member = ldap_get_member(username)
- if not ldap_is_evals(member):
+ member = ldap.get_member(username)
+ if not ldap.is_evals(member):
app.logger.warn('Stopped member {} from accessing admin UI'.format(username))
return redirect(app.config['PROTOCOL'] + app.config['PACKET_UPPER'], code=301)
else:
@@ -160,14 +157,15 @@ def create_new_packets(base_date: date, freshmen_list: dict):
print('Fetching data from LDAP...')
all_upper = list(filter(
- lambda member: not ldap_is_intromember(member) and not ldap_is_on_coop(member), ldap_get_active_members()))
+ lambda member: not ldap.is_intromember(member) and not ldap.is_on_coop(member), ldap.get_active_members()))
- rtp = ldap_get_active_rtps()
- three_da = ldap_get_3das()
- webmaster = ldap_get_webmasters()
- c_m = ldap_get_constitutional_maintainers()
- w_m = ldap_get_wiki_maintainers()
- drink = ldap_get_drink_admins()
+
+ rtp = ldap.get_active_rtps()
+ three_da = ldap.get_3das()
+ webmaster = ldap.get_webmasters()
+ c_m = ldap.get_constitutional_maintainers()
+ w_m = ldap.get_wiki_maintainers()
+ drink = ldap.get_drink_admins()
# Packet starting notifications
packets_starting_notification(start)
@@ -182,7 +180,7 @@ def create_new_packets(base_date: date, freshmen_list: dict):
for member in all_upper:
sig = UpperSignature(packet=packet, member=member.uid)
- sig.eboard = ldap_get_eboard_role(member)
+ sig.eboard = ldap.get_eboard_role(member)
sig.active_rtp = member.uid in rtp
sig.three_da = member.uid in three_da
sig.webmaster = member.uid in webmaster
@@ -201,20 +199,20 @@ def create_new_packets(base_date: date, freshmen_list: dict):
def sync_with_ldap():
print('Fetching data from LDAP...')
all_upper = {member.uid: member for member in filter(
- lambda member: not ldap_is_intromember(member) and not ldap_is_on_coop(member), ldap_get_active_members())}
+ lambda member: not ldap.is_intromember(member) and not ldap.is_on_coop(member), ldap.get_active_members())}
- rtp = ldap_get_active_rtps()
- three_da = ldap_get_3das()
- webmaster = ldap_get_webmasters()
- c_m = ldap_get_constitutional_maintainers()
- w_m = ldap_get_wiki_maintainers()
- drink = ldap_get_drink_admins()
+ rtp = ldap.get_active_rtps()
+ three_da = ldap.get_3das()
+ webmaster = ldap.get_webmasters()
+ c_m = ldap.get_constitutional_maintainers()
+ w_m = ldap.get_wiki_maintainers()
+ drink = ldap.get_drink_admins()
print('Applying updates to the DB...')
for packet in Packet.query.filter(Packet.end > datetime.now()).all():
# Update the role state of all UpperSignatures
for sig in filter(lambda sig: sig.member in all_upper, packet.upper_signatures):
- sig.eboard = ldap_get_eboard_role(all_upper[sig.member])
+ sig.eboard = ldap.get_eboard_role(all_upper[sig.member])
sig.active_rtp = sig.member in rtp
sig.three_da = sig.member in three_da
sig.webmaster = sig.member in webmaster
@@ -233,7 +231,7 @@ def sync_with_ldap():
for sig in filter(lambda sig: sig.member in all_upper, packet.misc_signatures):
MiscSignature.query.filter_by(packet_id=packet.id, member=sig.member).delete()
sig = UpperSignature(packet=packet, member=sig.member, signed=True)
- sig.eboard = ldap_get_eboard_role(all_upper[sig.member])
+ sig.eboard = ldap.get_eboard_role(all_upper[sig.member])
sig.active_rtp = sig.member in rtp
sig.three_da = sig.member in three_da
sig.webmaster = sig.member in webmaster
@@ -247,7 +245,7 @@ def sync_with_ldap():
upper_sigs = set(map(lambda sig: sig.member, packet.upper_signatures))
for member in filter(lambda member: member not in upper_sigs, all_upper):
sig = UpperSignature(packet=packet, member=member)
- sig.eboard = ldap_get_eboard_role(all_upper[sig.member])
+ sig.eboard = ldap.get_eboard_role(all_upper[sig.member])
sig.active_rtp = sig.member in rtp
sig.three_da = sig.member in three_da
sig.webmaster = sig.member in webmaster
diff --git a/requirements.txt b/requirements.txt
index d97e06f..01ab4db 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,13 @@
-Flask~=1.1.0
+Flask~=1.1.2
Flask-pyoidc~=2.2.0
Flask-Mail~=0.9.1
Flask-Gzip~=0.2
-flask_sqlalchemy~=2.3.2
-psycopg2-binary~=2.8.3
-Flask-Migrate~=2.2.1
-pylint~=2.3.1
-gunicorn~=19.7.1
-csh_ldap~=2.1.0
+flask_sqlalchemy~=2.4.4
+psycopg2-binary~=2.8.6
+Flask-Migrate~=2.5.3
+pylint~=2.6.0
+gunicorn~=20.0.4
+csh_ldap~=2.3.1
onesignal-sdk~=1.0.0
pylint-quotes~=0.2.1
-sentry-sdk~=0.14.2
+sentry-sdk~=0.19.5
diff --git a/yarn.lock b/yarn.lock
index f24d4cb..6863583 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1544,9 +1544,9 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1,
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@^1.3.4:
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
- integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
+ integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
interpret@^1.4.0:
version "1.4.0"