Skip to content

Commit

Permalink
Feat: Cal1Card Photo (#65)
Browse files Browse the repository at this point in the history
* some test routes

* fetching photo from c1c api

* feat: server photo cache

* c1c client & mock

* fix: remove unused imports
  • Loading branch information
Reimirno authored Apr 17, 2024
1 parent eed0536 commit 91f2861
Show file tree
Hide file tree
Showing 17 changed files with 200 additions and 250 deletions.
10 changes: 10 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ EMAIL_PORT=
EMAIL_USE_TLS=
EMAIL_USERNAME=

# Cal1Card API URL
MOCK_C1C=false
C1C_API_DOMAIN=
C1C_API_USERNAME=
C1C_API_PASSWORD=
# if using a proxy to access c1c, set the proxy URL here
C1C_PROXY_URL=
# in seconds, how long to cache c1c photos
C1C_PHOTO_CACHE_LIFE=7776000

# codecov token
# not needed if not using codecov
CODECOV_TOKEN=
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,6 @@ gcp-service-account-credentials.json

# bootstrap
setup*.sh

# cache
.cache/
8 changes: 8 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ def getenv(key, default: Optional[str] = None, *, optional=False):
EMAIL_USERNAME = getenv('EMAIL_USERNAME', optional=True)
EMAIL_PASSWORD = getenv('EMAIL_PASSWORD', optional=True)

# C1C API setup
MOCK_C1C = getenv('MOCK_C1C', 'false').lower() == 'true'
C1C_PROXY_URL = getenv('C1C_PROXY_URL', optional=True)
C1C_API_DOMAIN = getenv('C1C_API_DOMAIN', optional=True)
C1C_API_USERNAME = getenv('C1C_API_USERNAME', optional=True)
C1C_API_PASSWORD = getenv('C1C_API_PASSWORD', optional=True)
C1C_PHOTO_CACHE_LIFE = getenv('C1C_PHOTO_CACHE_LIFE', 3600 * 24 * 90)

# Master room sheet
MASTER_ROOM_SHEET_URL = getenv('MASTER_ROOM_SHEET_URL', optional=True)

Expand Down
180 changes: 0 additions & 180 deletions download_bcourses_photos.py

This file was deleted.

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ canvasapi==3.2.0
gunicorn==21.2.0
psycopg2==2.9.9
sentry-sdk[flask]
Flask-Caching==2.1.0
7 changes: 6 additions & 1 deletion server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,21 @@ def handle_invalid_access_token(e):
max=max,
)


# These must be done after `app`` is created as they depend on `app`

# prepare server cache
import server.cache # noqa

# prepares all auth clients
import server.services.auth # noqa

# prepares mock canvas db
import server.services.canvas.fake_data # noqa

# registers controller converters
from server.controllers import ExamConverter, OfferingConverter # noqa
from server.controllers import ExamConverter, OfferingConverter, StudentConverter # noqa
app.url_map.converters['exam_student'] = StudentConverter
app.url_map.converters['exam'] = ExamConverter
app.url_map.converters['offering'] = OfferingConverter

Expand Down
15 changes: 15 additions & 0 deletions server/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from flask_caching import Cache
from server import app

cache_store = Cache(app, config={
'CACHE_TYPE': 'FileSystemCache',
'CACHE_DIR': '.cache',
'CACHE_DEFAULT_TIMEOUT': 3600}
)


def cache_key_photo(canvas_id: str):
return f'student_photo_{canvas_id}'


cache_life_photo = int(app.config.get('C1C_PHOTO_CACHE_LIFE'))
40 changes: 38 additions & 2 deletions server/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,52 @@

GENERAL_STUDENT_HINT = "If you think this is a mistake, please contact your course staff."

ban_words = '(?!(((new)|(offerings)|(exams))\b))'
ban_words = r'(?!((new)|(offerings)|(exams)|(student)))'
offering_regex = ban_words + r'\d+'
exam_regex = ban_words + r'\w+'
student_regex = r'\d+'


def format_student_url(offering_canvas_id, exam_name, student_canvas_id):
return 'offerings/{}/exams/{}/students/{}'.format(offering_canvas_id, exam_name, student_canvas_id)


class StudentConverter(BaseConverter):
regex = format_student_url(offering_regex, exam_regex, student_regex)

def to_python(self, value):
if not current_user.is_authenticated:
session['after_login'] = request.url
raise Redirect(url_for('auth.login'))
_, offering_canvas_id, _, exam_name, _, student_canvas_id = value.split('/', 5)
exam = Exam.query.filter_by(
offering_canvas_id=offering_canvas_id, name=exam_name
).one_or_none()

if str(offering_canvas_id) in current_user.staff_offerings:
pass
elif str(offering_canvas_id) in current_user.student_offerings:
abort(403, "You are not authorized to view this page. " + GENERAL_STUDENT_HINT)
else:
abort(403, "You are not authorized to view this page. " + GENERAL_STUDENT_HINT)
exam_student = Student.query.filter_by(
canvas_id=student_canvas_id, exam_id=exam.id).one_or_none()
if not exam_student:
abort(404, "This student is not in this exam. ")
return exam, exam_student

def to_url(self, value):
exam, exam_student = value
rlt = format_student_url(exam.offering_canvas_id, exam.name, exam_student.canvas_id)
return rlt


def format_exam_url(offering_canvas_id, exam_name):
return 'offerings/{}/exams/{}'.format(offering_canvas_id, exam_name)


class ExamConverter(BaseConverter):
regex = format_exam_url(offering_regex, exam_regex)
regex = format_exam_url(offering_regex, exam_regex + r'(?!/students/\d+)')

def to_python(self, value):
if not current_user.is_authenticated:
Expand Down Expand Up @@ -81,6 +116,7 @@ def to_url(self, offering):
auth_module = Blueprint('auth', 'auth', url_prefix='/')
dev_login_module = Blueprint('dev_login', 'dev_login', url_prefix='/dev_login')
health_module = Blueprint('health', 'health', url_prefix='/health')
c1c_module = Blueprint('c1c', 'c1c', url_prefix='/c1c')

import server.controllers.auth_controllers # noqa
import server.controllers.dev_login_controllers # noqa
Expand Down
43 changes: 43 additions & 0 deletions server/services/c1c/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from server import app
from server.services.c1c import fake_data


def is_mock_c1c() -> bool:
return app.config['MOCK_C1C'] and \
app.config['FLASK_ENV'].lower() != 'production'


class C1C:
def __init__(self, proxy_url, api_domain, username, password):
self.proxy_dict = {
'http': proxy_url,
'https': proxy_url
} if proxy_url else None
self.api_domain = api_domain
self.username = username
self.password = password

def _make_request(self, path, method='GET'):
import requests
url = f'{self.api_domain}{path}'
if self.proxy_dict:
return requests.request(method, url, proxies=self.proxy_dict,
auth=(self.username, self.password))
else:
return requests.request(method, url, auth=(self.username, self.password))

def get_student_photo(self, student_canvas_id):
if is_mock_c1c():
return fake_data.get_fake_photo(student_canvas_id)
try:
r = self._make_request(f'/c1c-api/v1/photo/{student_canvas_id}')
if r.status_code == 200:
return r.content
else:
return None
except:
return None


c1c_client = C1C(app.config['C1C_PROXY_URL'], app.config['C1C_API_DOMAIN'],
app.config['C1C_API_USERNAME'], app.config['C1C_API_PASSWORD'])
18 changes: 18 additions & 0 deletions server/services/c1c/fake_data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import json

FAKE_PHOTO_DICT = None

if not FAKE_PHOTO_DICT:
with open('server/services/c1c/fake_data/photos.json', 'rb') as f:
FAKE_PHOTO_DICT = json.load(f)


def get_fake_photo(student_canvas_id):
print(FAKE_PHOTO_DICT)
try:
with open(f"server/services/c1c/fake_data/photos/{FAKE_PHOTO_DICT[student_canvas_id]}",
'rb') as f:
return f.read()
except Exception as e:
print(e)
return None
6 changes: 6 additions & 0 deletions server/services/c1c/fake_data/photos.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"123456": "sample1.jpg",
"234567": "sample1.jpg",
"345678": "sample1.jpg",
"456789": "sample1.jpg"
}
Binary file added server/services/c1c/fake_data/photos/sample1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 91f2861

Please sign in to comment.