Skip to content

Commit

Permalink
Merge branch 'master' into dependabot/pip/numpy-1.22.0
Browse files Browse the repository at this point in the history
  • Loading branch information
xcompass authored Oct 31, 2024
2 parents 40dd4b6 + 37f9377 commit e8d73cf
Show file tree
Hide file tree
Showing 22 changed files with 601 additions and 388 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ jobs:
- name: Publish to Registry
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
repository: ubcctlt/compair-app
tag_with_ref: true
- name: Trigger deploy
Expand Down
32 changes: 19 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
# v1.3
## Notable changes

* Upgrades: There is a high risk of regressions due to many upgrades to the libraries used by ComPAIR.

## Notable changes
* Many dependencies, both frontend and backend, were updated.

### New Features
* 1. "Download All" attachments button was added to generate and download a zip file of all submitted attachments in answers
* 2. The "Assignment End-Date" feature was added for admin users to query for the assignments end-date.

* "Download All" attachments button was added to generate and download a zip file of all student submitted answer attachments. This can be found in an assignment's "Participation" tab under the "Attachments" column.
* The "Assignment End-Date" tool was added for admin users to query for the assignments end-date.
* The purpose of this page is to search for ongoing or active assignments on a given date, to help plan potential schedules for testing, staging, and production environments.

### Environment Variable Changes
* CELERY_ALWAYS_EAGER is now CELERY_TASK_ALWAYS_EAGER
* Default: false

### New Environment Variables: For controlling memory leak growth in Kubernetes
### New Environment Variables: For controlling worker memory leak
* CELERY_WORKER_MAX_TASKS_PER_CHILD - Kills a worker process and forks a new one when it has executed the given number of tasks.
* Default to 20

* CELERY_WORKER_MAX_MEMORY_PER_CHILD - Set to memory in kilobytes. Kills a worker process and forks a new one when it hits the given memory usage, the currently executing task will be allowed to complete before being killed.
* Default to 600MB

## Breaking Changes
Celery 4 introduced a new all lowercase environment variables system. ComPAIR
is now using this new system. To adapt a Celery environment variable to
ComPAIR, convert the original Celery variable to all uppercase and prefix it
"CELERY\_". ComPAIR will strip the prefix and lowercase the variable before
passing it to Celery. A few Celery environment variables were renamed in the
new system, the ones supported in ComPAIR are:

* CELERY_ALWAYS_EAGER is now CELERY_TASK_ALWAYS_EAGER
* Set to true if running stock standalone, see `compair/settings.py`.
* Set to false if running via repo's docker-compose.yml
* BROKER_TRANSPORT_OPTIONS is now CELERY_BROKER_TRANSPORT_OPTIONS
* CELERYBEAT_SCHEDULE is now CELERY_BEAT_SCHEDULE

# v1.2.12

Expand Down
1 change: 1 addition & 0 deletions compair/api/assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def non_blank_text(value):
new_assignment_parser.add_argument('answer_end', required=True, nullable=False)
new_assignment_parser.add_argument('compare_start', default=None)
new_assignment_parser.add_argument('compare_end', default=None)
new_assignment_parser.add_argument('compare_localTimeZone', default='UTC')
new_assignment_parser.add_argument('self_eval_start', default=None)
new_assignment_parser.add_argument('self_eval_end', default=None)
new_assignment_parser.add_argument('self_eval_instructions', type=non_blank_text, default=None)
Expand Down
39 changes: 25 additions & 14 deletions compair/api/assignment_attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@
# differs completely from that used by file.py, I've had to split it out.


# given an assignment, download all attachments in that assignment.
# /api/courses/<course>/assignments/<assignment>/attachments/download
class DownloadAllAttachmentsAPI(Resource):
# given an assignment, download all student attachments in that assignment.
# This is restricted to only student answers to match the behaviour of the
# "Participation" tab in the UI, where it only lists students.
# /api/courses/<course>/assignments/<assignment>/attachments/download_students
class DownloadAllStudentAttachmentsAPI(Resource):
DELIM = ' - '

@login_required
def get(self, course_uuid, assignment_uuid):
# course unused, but we need to call it to check if it's a valid course
Expand All @@ -38,13 +42,21 @@ def get(self, course_uuid, assignment_uuid):
message="Sorry, your system role does not allow downloading all attachments")

# grab answers so we can see how many has files
answers = self.getAnswersByAssignment(assignment)
answers = self.getStudentAnswersByAssignment(assignment)
fileIds = []
fileAuthors = {}
for answer in answers:
if not answer.file_id:
continue
# answer has an attachment
fileIds.append(answer.file_id)
# the user who uploaded the file can be different from the answer
# author (e.g. instructor can upload on behalf of student), so
# we need to use the answer author instead of file uploader
author = answer.user_fullname
if answer.user_student_number:
author += self.DELIM + answer.user_student_number
fileAuthors[answer.file_id] = author

if not fileIds:
return {'msg': 'Assignment has no attachments'}
Expand All @@ -64,13 +76,9 @@ def get(self, course_uuid, assignment_uuid):
current_app.config['ATTACHMENT_UPLOAD_FOLDER'],
srcFile.name
)
# set filename to 'full name - student number - uuid.ext'
# omit student number or extension if not exist
delim = ' - '
srcFileName = srcFile.user.fullname
if srcFile.user.student_number:
srcFileName += delim + srcFile.user.student_number
srcFileName += delim + srcFile.name
# filename should be 'full name - student number - uuid.ext'
# student number is omitted if user doesn't have one
srcFileName = fileAuthors[srcFile.id] + self.DELIM + srcFile.name
#current_app.logger.debug("writing " + srcFileName)
zipFile.write(srcFilePath, srcFileName)
#current_app.logger.debug("Writing zip file")
Expand All @@ -79,7 +87,7 @@ def get(self, course_uuid, assignment_uuid):

# this really should be abstracted out into the Answer model, but I wasn't
# able to get the join with UserCourse to work inside the Answer model.
def getAnswersByAssignment(self, assignment):
def getStudentAnswersByAssignment(self, assignment):
return Answer.query \
.outerjoin(UserCourse, and_(
Answer.user_id == UserCourse.user_id,
Expand All @@ -91,7 +99,10 @@ def getAnswersByAssignment(self, assignment):
Answer.practice == False,
Answer.draft == False,
or_(
and_(UserCourse.course_role != CourseRole.dropped, Answer.user_id != None),
and_(
UserCourse.course_role == CourseRole.student,
Answer.user_id != None
),
Answer.group_id != None
)
)) \
Expand All @@ -102,4 +113,4 @@ def getFilesByIds(self, fileIds):
filter(File.id.in_(fileIds)).all()


api.add_resource(DownloadAllAttachmentsAPI, '/download')
api.add_resource(DownloadAllStudentAttachmentsAPI, '/download_students')
31 changes: 21 additions & 10 deletions compair/api/assignment_search_enddate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from sqlalchemy import create_engine
import json
from flask import jsonify
import pytz

from bouncer.constants import READ, EDIT, CREATE, DELETE, MANAGE
from flask import Blueprint, current_app
Expand All @@ -24,13 +25,11 @@
from .util import new_restful_api, get_model_changes, pagination_parser

from datetime import datetime
import time

assignment_search_enddate_api = Blueprint('assignment_search_enddate_api', __name__)
api = new_restful_api(assignment_search_enddate_api)

##event
on_assignment_get = event.signal('ASSIGNMENT_GET')

def validate(date_text):
try:
if date_text != datetime.strptime(date_text, "%Y-%m-%d").strftime('%Y-%m-%d'):
Expand All @@ -43,29 +42,41 @@ class AssignmentRootAPI1(Resource):
@login_required
def get(self):

# get app timezone in settings
appTimeZone = current_app.config.get('APP_TIMEZONE', time.strftime('%Z') )

search_date_assignment_parser = RequestParser()
search_date_assignment_parser.add_argument('compare_start', default=datetime.now().strftime("%Y-%m-%d"))
search_date_assignment_parser.add_argument('compare_end', default=datetime.now().strftime("%Y-%m-%d"))
search_date_assignment_parser.add_argument('compare_localTimeZone', default=appTimeZone)

args = search_date_assignment_parser.parse_args()

end_date = datetime.now().strftime("%Y-%m-%d")
end_date = datetime.now().strftime("%Y-%m-%d 00:00:00")
start_date = datetime.now().strftime("%Y-%m-%d")
compare_localTimeZone = appTimeZone

if (args['compare_localTimeZone']):
compare_localTimeZone = str(args['compare_localTimeZone'])

if validate(args['compare_end']):
end_date = str(args['compare_end'])
end_date = str(args['compare_end']) + ' 00:00:00'

##convert this to System TZ
local = pytz.timezone(compare_localTimeZone)
naive = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
local_dt = local.localize(naive, is_dst=None)
systemTZ_dt = local_dt.astimezone(pytz.timezone(appTimeZone))
end_date = str(systemTZ_dt)

if validate(args['compare_start']):
start_date = str(args['compare_start'])

db_url = str(current_app.config['SQLALCHEMY_DATABASE_URI'])
engine = create_engine(db_url, pool_size=5, pool_recycle=3600)
conn = engine.connect()
##sql_text = str("SELECT JSON_OBJECT('uuid', uuid,'name', name,'compare_start', compare_start, 'compare_end', compare_end) FROM assignment;");
##sql_text = str("SELECT JSON_OBJECT('uuid', uuid,'name', name,'compare_start', compare_start, 'compare_end', compare_end) FROM assignment WHERE compare_end >= '" + end_date + "';");
##sql_text = str("SELECT JSON_OBJECT('uuid', uuid,'name', name,'answer_start', answer_start,'answer_end', answer_end,'compare_start', compare_start, 'compare_end', compare_end) FROM assignment WHERE compare_end >= '" + end_date + "' OR answer_end >= '" + end_date + "';");
sql_text = str("SELECT JSON_OBJECT('uuid', uuid,'name', name,'answer_start', date_format(answer_start, '%%M %%d, %%Y'),'answer_end', date_format(answer_end, '%%M %%d, %%Y'),'compare_start', date_format(compare_start, '%%M %%d, %%Y'), 'compare_end', date_format(compare_end, '%%M %%d, %%Y')) FROM assignment WHERE compare_end >= '" + end_date + "' OR answer_end >= '" + end_date + "';");

##print(sql_text)
sql_text = str("SELECT JSON_OBJECT('course_name', t1.name,'name', t2.name,'answer_start', date_format(CONVERT_TZ(t2.answer_start, '" + appTimeZone + "','" + compare_localTimeZone + "'), '%%b %%d, %%Y'),'answer_end', date_format(CONVERT_TZ(t2.answer_end, '" + appTimeZone + "','" + compare_localTimeZone + "'), '%%b %%d, %%Y'),'compare_start', date_format(CONVERT_TZ(t2.compare_start, '" + appTimeZone + "','" + compare_localTimeZone + "'), '%%b %%d, %%Y'), 'compare_end', date_format(CONVERT_TZ(t2.compare_end, '" + appTimeZone + "','" + compare_localTimeZone + "'), '%%b %%d, %%Y'), 'self_eval_end', date_format(CONVERT_TZ(t2.self_eval_end, '" + appTimeZone + "','" + compare_localTimeZone + "'), '%%b %%d, %%Y'), 'self_eval_start', date_format(CONVERT_TZ(t2.self_eval_start, '" + appTimeZone + "','" + compare_localTimeZone + "'), '%%b %%d, %%Y')) FROM course as t1, assignment as t2 WHERE (t1.id = t2.course_id) AND (t2.active=TRUE AND t1.active=TRUE) AND (t2.compare_end >= '" + end_date + "' OR answer_end >= '" + end_date + "' OR self_eval_end >= '" + end_date + "');");

result = conn.execute(sql_text)

Expand Down
8 changes: 7 additions & 1 deletion compair/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import os
import json
import re
import pytz
import time

from distutils.util import strtobool
from flask import Config
Expand Down Expand Up @@ -91,7 +93,7 @@
'KALTURA_SECRET', 'KALTURA_PLAYER_ID',
'MAIL_SERVER', 'MAIL_DEBUG', 'MAIL_USERNAME', 'MAIL_PASSWORD',
'MAIL_DEFAULT_SENDER', 'MAIL_SUPPRESS_SEND',
'GA_TRACKING_ID'
'GA_TRACKING_ID', 'APP_TIMEZONE'
]

env_bool_overridables = [
Expand Down Expand Up @@ -150,3 +152,7 @@
config['APP_LOGIN_ENABLED'] = True
config['CAS_LOGIN_ENABLED'] = False
config['SAML_LOGIN_ENABLED'] = False

# configuring APP_TIMEZONE
if not(config['APP_TIMEZONE'] in pytz.all_timezones):
config['APP_TIMEZONE'] = time.strftime('%Z')
Loading

0 comments on commit e8d73cf

Please sign in to comment.