Skip to content

Commit f58bbc3

Browse files
jennifer-richardsNGPixel
authored andcommitted
ci: parameterize / update settings (ietf-tools#7248)
* ci: parameterize gunicorn in datatracker-start.sh * fix: typo * ci: update settings_local for helm chart * ci: Add todo comment * ci: Drop redundant USE_TZ setting * ci: Require secrets in production * ci: fix indentation * style: Black * ci: memcached cfg from env in settings.py * ci: set SITE_URL in settings.py * refactor: /www/htpasswd -> /a/www/htpasswd (it's a symlink on production) * refactor: Remove obsolete SECR_ settings * refactor: SECR_MAX_UPLOAD_SIZE -> DATATRACKER_... * refactor: SECR_PPT2PDF_COMMAND -> PPT2PDF_COMMAND * ci: Fix up helm/settings_local * ci: Remove commented-out settings * ci: Refactor/improve env var guards * ci: More env refactoring / guards
1 parent e3d0290 commit f58bbc3

7 files changed

Lines changed: 275 additions & 144 deletions

File tree

dev/build/datatracker-start.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ echo "Running collectstatic..."
1212
echo "Starting Datatracker..."
1313

1414
gunicorn \
15-
--workers 9 \
16-
--max-requests 32768 \
17-
--timeout 180 \
15+
--workers ${DATATRACKER_GUNICORN_WORKERS:-9} \
16+
--max-requests ${DATATRACKER_GUNICORN_MAX_REQUESTS:-32768} \
17+
--timeout ${DATATRACKER_GUNICORN_TIMEOUT:-180} \
1818
--bind :8000 \
19-
--log-level info \
19+
--log-level ${DATATRACKER_GUNICORN_LOG_LEVEL:-info} \
2020
ietf.wsgi:application
2121

2222
# Leaving this here as a reminder to set up the env in the chart

helm/settings_local.py

Lines changed: 168 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,200 @@
11
# Copyright The IETF Trust 2007-2024, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

4+
from base64 import b64decode
5+
from email.utils import parseaddr
6+
7+
from ietf import __release_hash__
48
from ietf.settings import * # pyflakes:ignore
59

6-
ALLOWED_HOSTS = ['*']
10+
11+
# Default to "development". Production _must_ set DATATRACKER_SERVER_MODE="production" in the env!
12+
SERVER_MODE = os.environ.get("DATATRACKER_SERVER_MODE", "development")
13+
14+
# Secrets
15+
_SECRET_KEY = os.environ.get("DATATRACKER_DJANGO_SECRET_KEY", None)
16+
if _SECRET_KEY is not None:
17+
SECRET_KEY = _SECRET_KEY
18+
elif SERVER_MODE == "production":
19+
raise RuntimeError("DATATRACKER_DJANGO_SECRET_KEY must be set in production")
20+
21+
_NOMCOM_APP_SECRET_B64 = os.environ.get("DATATRACKER_NOMCOM_APP_SECRET_B64", None)
22+
if _NOMCOM_APP_SECRET_B64 is not None:
23+
NOMCOM_APP_SECRET = b64decode(_NOMCOM_APP_SECRET_B64)
24+
elif SERVER_MODE == "production":
25+
raise RuntimeError("DATATRACKER_NOMCOM_APP_SECRET_B64 must be set in production")
26+
27+
_IANA_SYNC_PASSWORD = os.environ.get("DATATRACKER_IANA_SYNC_PASSWORD", None)
28+
if _IANA_SYNC_PASSWORD is not None:
29+
IANA_SYNC_PASSWORD = _IANA_SYNC_PASSWORD
30+
elif SERVER_MODE == "production":
31+
raise RuntimeError("DATATRACKER_IANA_SYNC_PASSWORD must be set in production")
32+
33+
_RFC_EDITOR_SYNC_PASSWORD = os.environ.get("DATATRACKER_RFC_EDITOR_SYNC_PASSWORD", None)
34+
if _RFC_EDITOR_SYNC_PASSWORD is not None:
35+
RFC_EDITOR_SYNC_PASSWORD = os.environ.get("DATATRACKER_RFC_EDITOR_SYNC_PASSWORD")
36+
elif SERVER_MODE == "production":
37+
raise RuntimeError("DATATRACKER_RFC_EDITOR_SYNC_PASSWORD must be set in production")
38+
39+
_YOUTUBE_API_KEY = os.environ.get("DATATRACKER_YOUTUBE_API_KEY", None)
40+
if _YOUTUBE_API_KEY is not None:
41+
YOUTUBE_API_KEY = _YOUTUBE_API_KEY
42+
elif SERVER_MODE == "production":
43+
raise RuntimeError("DATATRACKER_YOUTUBE_API_KEY must be set in production")
44+
45+
_GITHUB_BACKUP_API_KEY = os.environ.get("DATATRACKER_GITHUB_BACKUP_API_KEY", None)
46+
if _GITHUB_BACKUP_API_KEY is not None:
47+
GITHUB_BACKUP_API_KEY = _GITHUB_BACKUP_API_KEY
48+
elif SERVER_MODE == "production":
49+
raise RuntimeError("DATATRACKER_GITHUB_BACKUP_API_KEY must be set in production")
50+
51+
_API_KEY_TYPE = os.environ.get("DATATRACKER_API_KEY_TYPE", None)
52+
if _API_KEY_TYPE is not None:
53+
API_KEY_TYPE = _API_KEY_TYPE
54+
elif SERVER_MODE == "production":
55+
raise RuntimeError("DATATRACKER_API_KEY_TYPE must be set in production")
56+
57+
_API_PUBLIC_KEY_PEM_B64 = os.environ.get("DATATRACKER_API_PUBLIC_KEY_PEM_B64", None)
58+
if _API_PUBLIC_KEY_PEM_B64 is not None:
59+
API_PUBLIC_KEY_PEM = b64decode(_API_PUBLIC_KEY_PEM_B64)
60+
elif SERVER_MODE == "production":
61+
raise RuntimeError("DATATRACKER_API_PUBLIC_KEY_PEM_B64 must be set in production")
62+
63+
_API_PRIVATE_KEY_PEM_B64 = os.environ.get("DATATRACKER_API_PRIVATE_KEY_PEM_B64", None)
64+
if _API_PRIVATE_KEY_PEM_B64 is not None:
65+
API_PRIVATE_KEY_PEM = b64decode(_API_PRIVATE_KEY_PEM_B64)
66+
elif SERVER_MODE == "production":
67+
raise RuntimeError("DATATRACKER_API_PRIVATE_KEY_PEM_B64 must be set in production")
68+
69+
# Set DEBUG if DATATRACKER_DEBUG env var is the word "true"
70+
DEBUG = os.environ.get("DATATRACKER_DEBUG", "false").lower() == "true"
71+
72+
# DATATRACKER_ALLOWED_HOSTS env var is a comma-separated list of allowed hosts
73+
_allowed_hosts_str = os.environ.get("DATATRACKER_ALLOWED_HOSTS", None)
74+
if _allowed_hosts_str is not None:
75+
ALLOWED_HOSTS = [h.strip() for h in _allowed_hosts_str.split(",")]
776

877
DATABASES = {
978
"default": {
10-
"HOST": os.environ.get("DBHOST", "db"),
11-
"PORT": os.environ.get("DBPORT", "5432"),
12-
"NAME": os.environ.get("DBNAME", "datatracker"),
79+
"HOST": os.environ.get("DATATRACKER_DBHOST", "db"),
80+
"PORT": os.environ.get("DATATRACKER_DBPORT", "5432"),
81+
"NAME": os.environ.get("DATATRACKER_DBNAME", "datatracker"),
1382
"ENGINE": "django.db.backends.postgresql",
14-
"USER": os.environ.get("DBUSER", "django"),
15-
"PASSWORD": os.environ.get("DBPASS", ""),
83+
"USER": os.environ.get("DATATRACKER_DBUSER", "django"),
84+
"PASSWORD": os.environ.get("DATATRACKER_DBPASS", ""),
1685
},
1786
}
1887

19-
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
88+
# DATATRACKER_ADMINS is a newline-delimited list of addresses parseable by email.utils.parseaddr
89+
_ADMINS = os.environ.get("DATATRACKER_ADMINS", None)
90+
if _ADMINS is not None:
91+
ADMINS = [parseaddr(admin) for admin in _ADMINS.split("\n")]
92+
elif SERVER_MODE == "production":
93+
raise RuntimeError("DATATRACKER_ADMINS must be set in production")
94+
95+
USING_DEBUG_EMAIL_SERVER = os.environ.get("DATATRACKER_EMAIL_DEBUG", "false").lower() == "true"
96+
EMAIL_HOST = os.environ.get("DATATRACKER_EMAIL_HOST", "localhost")
97+
EMAIL_PORT = int(os.environ.get("DATATRACKER_EMAIL_PORT", "2025"))
2098

2199
CELERY_BROKER_URL = "amqp://datatracker:{password}@{host}/{queue}".format(
22100
host=os.environ.get("RABBITMQ_HOSTNAME", "rabbitmq"),
23101
password=os.environ.get("CELERY_PASSWORD", ""),
24102
queue=os.environ.get("RABBITMQ_QUEUE", "dt")
25103
)
26104

27-
IDSUBMIT_IDNITS_BINARY = "/usr/local/bin/idnits"
28-
IDSUBMIT_REPOSITORY_PATH = "/test/id/"
29-
IDSUBMIT_STAGING_PATH = "/test/staging/"
105+
IANA_SYNC_USERNAME = "ietfsync"
106+
IANA_SYNC_CHANGES_URL = "https://datatracker.iana.org:4443/data-tracker/changes"
107+
IANA_SYNC_PROTOCOLS_URL = "http://www.iana.org/protocols/"
30108

31-
AGENDA_PATH = "/assets/www6s/proceedings/"
32-
MEETINGHOST_LOGO_PATH = AGENDA_PATH
109+
RFC_EDITOR_NOTIFICATION_URL = "http://www.rfc-editor.org/parser/parser.php"
33110

34-
USING_DEBUG_EMAIL_SERVER=True
35-
EMAIL_HOST= "localhost"
36-
EMAIL_PORT=2025
111+
STATS_REGISTRATION_ATTENDEES_JSON_URL = 'https://registration.ietf.org/{number}/attendees/?apikey=redacted'
37112

38-
MEDIA_BASE_DIR = "/assets"
39-
MEDIA_ROOT = MEDIA_BASE_DIR + "/media/"
40-
MEDIA_URL = "/media/"
113+
#FIRST_CUTOFF_DAYS = 12
114+
#SECOND_CUTOFF_DAYS = 12
115+
#SUBMISSION_CUTOFF_DAYS = 26
116+
#SUBMISSION_CORRECTION_DAYS = 57
117+
MEETING_MATERIALS_SUBMISSION_CUTOFF_DAYS = 26
118+
MEETING_MATERIALS_SUBMISSION_CORRECTION_DAYS = 54
119+
120+
HTPASSWD_COMMAND = "/usr/bin/htpasswd2"
121+
122+
_MEETECHO_CLIENT_ID = os.environ.get("DATATRACKER_MEETECHO_CLIENT_ID", None)
123+
_MEETECHO_CLIENT_SECRET = os.environ.get("DATATRACKER_MEETECHO_CLIENT_SECRET", None)
124+
if _MEETECHO_CLIENT_ID is not None and _MEETECHO_CLIENT_SECRET is not None:
125+
MEETECHO_API_CONFIG = {
126+
"api_base": os.environ.get(
127+
"DATATRACKER_MEETECHO_API_BASE",
128+
"https://meetings.conf.meetecho.com/api/v1/",
129+
),
130+
"client_id": _MEETECHO_CLIENT_ID,
131+
"client_secret": _MEETECHO_CLIENT_SECRET,
132+
"request_timeout": 3.01, # python-requests doc recommend slightly > a multiple of 3 seconds
133+
}
134+
elif SERVER_MODE == "production":
135+
raise RuntimeError(
136+
"DATATRACKER_MEETECHO_CLIENT_ID and DATATRACKER_MEETECHO_CLIENT_SECRET must be set in production"
137+
)
138+
139+
APP_API_TOKENS = {
140+
"ietf.api.views.directauth": ["redacted",],
141+
"ietf.api.views.email_aliases": ["redacted"],
142+
"ietf.api.views.active_email_list": ["redacted"],
143+
}
41144

145+
EMAIL_COPY_TO = ""
146+
147+
# Until we teach the datatracker to look beyond cloudflare for this check
148+
IDSUBMIT_MAX_DAILY_SAME_SUBMITTER = 5000
149+
150+
# Leave DATATRACKER_MATOMO_SITE_ID unset to disable Matomo reporting
151+
if "DATATRACKER_MATOMO_SITE_ID" in os.environ:
152+
MATOMO_DOMAIN_PATH = os.environ.get("DATATRACKER_MATOMO_DOMAIN_PATH", "analytics.ietf.org")
153+
MATOMO_SITE_ID = os.environ.get("DATATRACKER_MATOMO_SITE_ID")
154+
MATOMO_DISABLE_COOKIES = True
155+
156+
# Leave DATATRACKER_SCOUT_KEY unset to disable Scout APM agent
157+
_SCOUT_KEY = os.environ.get("DATATRACKER_SCOUT_KEY", None)
158+
if _SCOUT_KEY is not None:
159+
if SERVER_MODE == "production":
160+
PROD_PRE_APPS = ["scout_apm.django", ]
161+
else:
162+
DEV_PRE_APPS = ["scout_apm.django", ]
163+
SCOUT_MONITOR = True
164+
SCOUT_KEY = _SCOUT_KEY
165+
SCOUT_NAME = "Datatracker"
166+
SCOUT_ERRORS_ENABLED = True
167+
SCOUT_SHUTDOWN_MESSAGE_ENABLED = False
168+
SCOUT_CORE_AGENT_DIR = "/a/core-agent/1.4.0"
169+
SCOUT_CORE_AGENT_FULL_NAME = "scout_apm_core-v1.4.0-x86_64-unknown-linux-musl"
170+
SCOUT_CORE_AGENT_SOCKET_PATH = "tcp://{host}:{port}".format(
171+
host=os.environ.get("DATATRACKER_SCOUT_CORE_AGENT_HOST", "scout"),
172+
port=os.environ.get("DATATRACKER_SCOUT_CORE_AGENT_PORT", "16590"),
173+
),
174+
SCOUT_CORE_AGENT_DOWNLOAD = False
175+
SCOUT_CORE_AGENT_LAUNCH = False
176+
SCOUT_REVISION_SHA = __release_hash__[:7]
177+
178+
# Path to the email alias lists. Used by ietf.utils.aliases
179+
DRAFT_ALIASES_PATH = "/a/postfix/draft-aliases"
180+
DRAFT_VIRTUAL_PATH = "/a/postfix/draft-virtual"
181+
GROUP_ALIASES_PATH = "/a/postfix/group-aliases"
182+
GROUP_VIRTUAL_PATH = "/a/postfix/group-virtual"
183+
184+
# Set these to the same as "production" in settings.py, whether production mode or not
185+
MEDIA_ROOT = "/a/www/www6s/lib/dt/media/"
186+
MEDIA_URL = "https://www.ietf.org/lib/dt/media/"
42187
PHOTOS_DIRNAME = "photo"
43188
PHOTOS_DIR = MEDIA_ROOT + PHOTOS_DIRNAME
44189

45-
SUBMIT_YANG_CATALOG_MODEL_DIR = "/assets/ietf-ftp/yang/catalogmod/"
46-
SUBMIT_YANG_DRAFT_MODEL_DIR = "/assets/ietf-ftp/yang/draftmod/"
47-
SUBMIT_YANG_INVAL_MODEL_DIR = "/assets/ietf-ftp/yang/invalmod/"
48-
SUBMIT_YANG_IANA_MODEL_DIR = "/assets/ietf-ftp/yang/ianamod/"
49-
SUBMIT_YANG_RFC_MODEL_DIR = "/assets/ietf-ftp/yang/rfcmod/"
50-
51-
# Set INTERNAL_IPS for use within Docker. See https://knasmueller.net/fix-djangos-debug-toolbar-not-showing-inside-docker
52-
import socket
53-
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
54-
INTERNAL_IPS = [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips]
55-
56-
# DEV_TEMPLATE_CONTEXT_PROCESSORS = [
57-
# 'ietf.context_processors.sql_debug',
58-
# ]
59-
60-
DOCUMENT_PATH_PATTERN = "/assets/ietfdata/doc/{doc.type_id}/"
61-
INTERNET_DRAFT_PATH = "/assets/ietf-ftp/internet-drafts/"
62-
RFC_PATH = "/assets/ietf-ftp/rfc/"
63-
CHARTER_PATH = "/assets/ietf-ftp/charter/"
64-
BOFREQ_PATH = "/assets/ietf-ftp/bofreq/"
65-
CONFLICT_REVIEW_PATH = "/assets/ietf-ftp/conflict-reviews/"
66-
STATUS_CHANGE_PATH = "/assets/ietf-ftp/status-changes/"
67-
INTERNET_DRAFT_ARCHIVE_DIR = "/assets/archive/id"
68-
INTERNET_ALL_DRAFTS_ARCHIVE_DIR = "/assets/archive/id"
69-
BIBXML_BASE_PATH = "/assets/ietfdata/derived/bibxml"
70-
IDSUBMIT_REPOSITORY_PATH = INTERNET_DRAFT_PATH
71-
72-
NOMCOM_PUBLIC_KEYS_DIR = "data/nomcom_keys/public_keys/"
73-
SLIDE_STAGING_PATH = "/test/staging/"
190+
# Normally only set for debug, but needed until we have a real FS
191+
DJANGO_VITE_MANIFEST_PATH = os.path.join(BASE_DIR, 'static/dist-neue/manifest.json')
74192

193+
# Binaries that are different in the docker image
75194
DE_GFM_BINARY = "/usr/local/bin/de-gfm"
195+
IDSUBMIT_IDNITS_BINARY = "/usr/local/bin/idnits"
76196

77-
# OIDC configuration
78-
SITE_URL = os.environ.get("OIDC_SITE_URL")
79-
80-
# todo: parameterize memcached url in settings.py
81-
MEMCACHED_HOST = os.environ.get("MEMCACHED_SERVICE_HOST", "127.0.0.1")
82-
MEMCACHED_PORT = os.environ.get("MEMCACHED_SERVICE_PORT", "11211")
197+
# Duplicating production cache from settings.py and using it whether we're in production mode or not
83198
from ietf import __version__
84199
CACHES = {
85200
"default": {
@@ -119,6 +234,3 @@
119234
},
120235
},
121236
}
122-
123-
# Normally only set for debug, but needed until we have a real FS
124-
DJANGO_VITE_MANIFEST_PATH = os.path.join(BASE_DIR, 'static/dist-neue/manifest.json')

helm/values.yaml

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -561,10 +561,35 @@ autoscaling:
561561

562562
env:
563563
# n.b., these are debug values / non-secret secrets
564-
# DBHOST: "host.minikube.internal"
565-
# DBPORT: "5432"
566-
DBNAME: "datatracker"
567-
DBUSER: "django"
568-
DBPASS: "RkTkDPFnKpko"
569-
DJANGO_SECRET_KEY: "PDwXboUq!=hPjnrtG2=ge#N$Dwy+wn@uivrugwpic8mxyPfHk"
564+
DATATRACKER_SERVER_MODE: "development" # defaults to "production"
565+
DATATRACKER_ADMINS: |-
566+
Robert Sparks <rjsparks@nostrum.com>
567+
Ryan Cross <rcross@amsl.com>
568+
Kesara Rathnayake <kesara@staff.ietf.org>
569+
Jennifer Richards <jennifer@staff.ietf.org>
570+
Nicolas Giard <nick@staff.ietf.org>
571+
DATATRACKER_ALLOWED_HOSTS: "*" # empty for production
572+
# DATATRACKER_DATATRACKER_DEBUG: "false"
573+
# DATATRACKER_DBHOST: "db"
574+
# DATATRACKER_DBPORT: "5432"
575+
# DATATRACKER_DBNAME: "datatracker"
576+
# DATATRACKER_DBUSER: "django"
577+
DATATRACKER_DBPASS: "RkTkDPFnKpko"
578+
DATATRACKER_DJANGO_SECRET_KEY: "PDwXboUq!=hPjnrtG2=ge#N$Dwy+wn@uivrugwpic8mxyPfHk"
579+
DATATRACKER_EMAIL_DEBUG: "true"
580+
DATATRACKER_EMAIL_HOST: "localhost"
581+
DATATRACKER_EMAIL_PORT: "2025"
582+
# DATATRACKER_NOMCOM_APP_SECRET_B64: "<base64-encoded bytes>"
583+
DATATRACKER_IANA_SYNC_PASSWORD: "this-is-the-iana-sync-password"
584+
DATATRACKER_RFC_EDITOR_SYNC_PASSWORD: "this-is-the-rfc-editor-sync-password"
585+
DATATRACKER_YOUTUBE_API_KEY: "this-is-the-youtube-api-key"
586+
DATATRACKER_GITHUB_BACKUP_API_KEY: "this-is-the-github-backup-api-key"
587+
# DATATRACKER_API_KEY_TYPE: "ES265"
588+
# DATATRACKER_API_PUBLIC_KEY_PEM_B64: "<base64-encoded PEM"
589+
# DATATRACKER_API_PRIVATE_KEY_PEM_B64: "<base64-encoded PEM"
590+
# DATATRACKER_MEETECHO_API_BASE: "https://meetings.conf.meetecho.com/api/v1/"
591+
DATATRACKER_MEETECHO_CLIENT_ID: "this-is-the-meetecho-client-id"
592+
DATATRACKER_MEETECHO_CLIENT_SECRET: "this-is-the-meetecho-client-secret"
593+
# DATATRACKER_MATOMO_SITE_ID: "7" # must be present to enable Matomo
594+
# DATATRACKER_MATOMO_DOMAIN_PATH: "analytics.ietf.org"
570595
CELERY_PASSWORD: "this-is-a-secret"

ietf/meeting/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -949,9 +949,9 @@ def post_process(doc):
949949
Does post processing on uploaded file.
950950
- Convert PPT to PDF
951951
'''
952-
if is_powerpoint(doc) and hasattr(settings, 'SECR_PPT2PDF_COMMAND'):
952+
if is_powerpoint(doc) and hasattr(settings, 'PPT2PDF_COMMAND'):
953953
try:
954-
cmd = list(settings.SECR_PPT2PDF_COMMAND) # Don't operate on the list actually in settings
954+
cmd = list(settings.PPT2PDF_COMMAND) # Don't operate on the list actually in settings
955955
cmd.append(doc.get_file_path()) # outdir
956956
cmd.append(os.path.join(doc.get_file_path(), doc.uploaded_filename)) # filename
957957
subprocess.check_call(cmd)

ietf/secr/meetings/tests.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,6 @@
2727

2828
class SecrMeetingTestCase(TestCase):
2929
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
30-
def setUp(self):
31-
super().setUp()
32-
self.bluesheet_dir = self.tempdir('bluesheet')
33-
self.bluesheet_path = os.path.join(self.bluesheet_dir,'blue_sheet.rtf')
34-
self.saved_secr_blue_sheet_path = settings.SECR_BLUE_SHEET_PATH
35-
settings.SECR_BLUE_SHEET_PATH = self.bluesheet_path
36-
37-
# n.b., the bluesheet upload relies on SECR_PROCEEDINGS_DIR being the same
38-
# as AGENDA_PATH. This is probably a bug, but may not be worth fixing if
39-
# the secr app is on the way out.
40-
self.saved_secr_proceedings_dir = settings.SECR_PROCEEDINGS_DIR
41-
settings.SECR_PROCEEDINGS_DIR = settings.AGENDA_PATH
42-
43-
def tearDown(self):
44-
settings.SECR_PROCEEDINGS_DIR = self.saved_secr_proceedings_dir
45-
settings.SECR_BLUE_SHEET_PATH = self.saved_secr_blue_sheet_path
46-
shutil.rmtree(self.bluesheet_dir)
47-
super().tearDown()
4830

4931
def test_main(self):
5032
"Main Test"
@@ -416,4 +398,4 @@ def test_get_times(self):
416398
times = get_times(meeting,day)
417399
values = [ x[0] for x in times ]
418400
self.assertTrue(times)
419-
self.assertTrue(timeslot.time.strftime('%H%M') in values)
401+
self.assertTrue(timeslot.time.strftime('%H%M') in values)

0 commit comments

Comments
 (0)