diff --git a/VERSION b/VERSION index 209e3ef4..aabe6ec3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -20 +21 diff --git a/front_end/CodeBuddy_dump.sql b/front_end/CodeBuddy_dump.sql new file mode 100644 index 00000000..e69de29b diff --git a/front_end/Containerfile b/front_end/Containerfile index a5894286..2b8e1a6e 100755 --- a/front_end/Containerfile +++ b/front_end/Containerfile @@ -74,5 +74,7 @@ RUN bash /app/build_html.sh ADD front_end/handlers/*.py /app/ ADD front_end/*.py /app/ +RUN bash /app/build_html.sh + #ENTRYPOINT ["bash"] ENTRYPOINT ["bash", "/app/startup.sh"] diff --git a/front_end/content.py b/front_end/content.py index fddb6210..4f6e6f04 100644 --- a/front_end/content.py +++ b/front_end/content.py @@ -24,7 +24,7 @@ BLANK_IMAGE = "/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/2wBDAQICAgICAgUDAwUKBwYHCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgr/wAARCALQA8ADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9/KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z" -class Content: +class ContentSQLite: def __init__(self, settings_dict): self.__settings_dict = settings_dict @@ -66,11 +66,18 @@ def fetchall(self, sql, params=()): return result + def dump(self,): + return self.conn.iterdump() + # This function creates tables as they were in version 5. Subsequent changes # to the database are implemented as migration scripts. def create_database_tables(self): self.execute('''CREATE TABLE IF NOT EXISTS metadata (version integer NOT NULL);''') - self.execute('''INSERT INTO metadata (version) VALUES (5);''') + + num_metadata_rows = self.fetchall('''SELECT COUNT(*) FROM metadata;''')[0][0] + + if num_metadata_rows == 0: + self.execute('''INSERT INTO metadata (version) VALUES (5);''') self.execute('''CREATE TABLE IF NOT EXISTS users ( user_id text PRIMARY KEY, @@ -2189,3 +2196,130 @@ def rerun_submissions(self, assignment_title=None): progress_file.write(f"Done rerunning submissions.\n") progress_file.flush() + + # This is a helper function used by dump_database + def line_is_useless(self, line): + useless_es = [ + 'BEGIN TRANSACTION', + 'COMMIT', + 'sqlite_sequence', + 'CREATE UNIQUE INDEX', + 'PRAGMA foreign_keys=OFF', + ] + for useless in useless_es: + if re.search(useless, line): + return True + + def dump_database(self,): + # Much of this code is based off of the code found in this exchange so the syntax is a little different than I would have used + # https://stackoverflow.com/questions/18671/quick-easy-way-to-migrate-sqlite3-to-mysql + + sqlite_dump_raw = '/logs/CodeBuddy_dump_raw.sql' + sqlite_dump_parsed = '/logs/CodeBuddy_dump.sql' + + # Writes the initial raw sqlite dump to a file. + f = open(sqlite_dump_raw, 'w') + for line in self.conn.iterdump(): + f.write('%s\n' % line) + f.close() + + # Begin parsing the raw sqlite dump. + with open(sqlite_dump_parsed, 'w') as dumpfile: + with open(sqlite_dump_raw) as raw: + inside_create_statement = False + add_comma = False + + for line in raw: + if self.line_is_useless(line): + continue + + # this line was necessary because ''); + # would be converted to \'); which isn't appropriate + if re.match(r".*, ''\);", line): + line = re.sub(r"''\);", r'``);', line) + + if re.match(r'^CREATE TABLE.*', line): + inside_create_statement = True + + m = re.search('CREATE TABLE IF NOT EXISTS "?(\w*)"?(.*)', line) + n = re.search('CREATE TABLE "?(\w*)"?(.*)', line) + if m: + name, sub = m.groups() + # Replaces double quotation marks with backticks per MYSQL specification in CREATE statements + line = "CREATE TABLE IF NOT EXISTS `%(name)s`%(sub)s\n" + line = line % dict(name=name, sub=sub) + elif n: + name, sub = n.groups() + # Replaces double quotation marks with backticks per MYSQL specification in CREATE statements + line = "CREATE TABLE IF NOT EXISTS `%(name)s`%(sub)s\n" + line = line % dict(name=name, sub=sub) + else: + m = re.search('INSERT INTO "(\w*)"(.*)', line) + if m: + # Replaces double quotation marks with backticks per MYSQL specification in INSERT statements + line = 'INSERT INTO `%s`%s\n' % m.groups() + line = line.replace('"', r'\"') + + if inside_create_statement: + # Makes sure that large columns are allocated more space. + line = line.replace("data_files text", "data_files mediumtext") + line = line.replace("image_output text", "image_output mediumtext") + line = line.replace("answer_code text", "answer_code longtext") + + # I think MYSQL didn't like that a text field was being used as a PRIMARY KEY + if "id text" in line: + line = re.sub("id text", "id int", line) + line = re.sub("user_id int", "user_id varchar(255)", line) + + # Adds auto_increment if it is not there since sqlite auto_increments all primary keys + if re.search(r"integer(?:\s+\w+)*\s*PRIMARY KEY(?:\s+\w+)*\s*,", line): + line = line.replace("PRIMARY KEY AUTOINCREMENT", "PRIMARY KEY AUTO_INCREMENT") + line = line.replace("PRIMARY KEY,", "PRIMARY KEY AUTO_INCREMENT,") + + m = re.search(r"FOREIGN KEY", line) + if m: + # Formats foreign key constraints according to MYSQL specifications + substring = re.search(r"FOREIGN KEY \((\w+)(\s*)", line).groups()[0] + line = re.sub(r"FOREIGN KEY \((\w+)(\s*)", r"CONSTRAINT foreign_key_\1 FOREIGN KEY (\1\2", line) + line = line.replace(f"foreign_key_{substring}", f"foreign_key_{substring}_{name}") + + # Sometimes sqlite leaves off a comma in between foreign key constraints so this block is neccessary to put them back in. + if add_comma: + line = f", {line}" + lastchar = line.strip()[-1] + if lastchar != "," and lastchar != ";": + add_comma = True + + # Replaces " and ' with ` because MYSQL doesn't like quotes in CREATE commands + if line.find('DEFAULT') == -1: + line = line.replace(r'"', r'`').replace(r"'", r'`') + else: + parts = line.split('DEFAULT') + parts[0] = parts[0].replace(r'"', r'`').replace(r"'", r'`') + line = 'DEFAULT'.join(parts) + + # And now we convert it back (see above) + if re.match(r".*, ``\);", line): + line = re.sub(r'``\);', r"'');", line) + + if inside_create_statement and re.match(r'.*\);', line): + inside_create_statement = False + add_comma = False + + if re.match(r"CREATE INDEX", line): + line = re.sub('"', '`', line) + + # Formats AUTOINCREMENT per MYSQL specifications + if re.match(r"AUTOINCREMENT", line): + line = re.sub("AUTOINCREMENT", "AUTO_INCREMENT", line) + + # If not inside create statement + else: + # For some reason sqlite often doubles up on quote marks in formatting their dumps. This converts double '' marks not indicating a blank sql value to \'. + line = re.sub(r"([^,])''([^,])", r"\1\'\2", line) + + + dumpfile.write(line + "\n") + + # Returns the name of the parsed file + return sqlite_dump_parsed diff --git a/front_end/content_maria.py b/front_end/content_maria.py new file mode 100644 index 00000000..90409d09 --- /dev/null +++ b/front_end/content_maria.py @@ -0,0 +1,2144 @@ +import atexit +from datetime import datetime, timezone +import glob +import gzip +from helper import * +import html +from imgcompare import * +import io +import json +import math +import os +import re +import spacy +import mariadb +import yaml +from yaml import load +from yaml import Loader +import zipfile + +# IMPORTANT: When creating/modifying queries that include any user input, +# please follow the recommendations on this page: +# https://realpython.com/prevent-python-sql-injection/ + +BLANK_IMAGE = "/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/2wBDAQICAgICAgUDAwUKBwYHCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgr/wAARCALQA8ADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9/KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z" + +class Content: + def __init__(self, settings_dict): + self.__settings_dict = settings_dict + + HOST = "127.0.0.1" + PORT = 3304 + + with open("secrets/MARIADB_USER") as pfile: + USERNAME = pfile.read().strip("\n") + + with open("secrets/MARIADB_PASSWORD") as pfile: + PASSWORD = pfile.read().strip("\n") + + with open("secrets/MARIADB_DATABASE") as pfile: + DATABASE = pfile.read().strip("\n") + + # Connect to MariaDB Platform + try: + self.conn = mariadb.connect( + user=USERNAME, + password=PASSWORD, + host=HOST, + port=PORT, + database=DATABASE + ) + print('success:)') + except mariadb.Error as e: + print(f"Error connecting to MariaDB Platform: {e}") + sys.exit(1) + + atexit.register(self.close) + + def close(self): + self.conn.close() + + def execute(self, sql, params=()): + cursor = self.conn.cursor() + cursor.execute(sql, params) + lastrowid = cursor.lastrowid + cursor.close() + + return lastrowid + + def fetchone(self, sql, params=()): + cursor = self.conn.cursor() + cursor.execute(sql, params) + result = cursor.fetchone() + cursor.close() + + return result + + def fetchall(self, sql, params=()): + cursor = self.conn.cursor() + cursor.execute(sql, params) + result = cursor.fetchall() + cursor.close() + + return result + + # This function creates tables as they were in version 5. Subsequent changes + # to the database are implemented as migration scripts. + def create_database_tables(self): + self.execute('''CREATE TABLE IF NOT EXISTS metadata (version integer NOT NULL);''') + self.execute('''INSERT INTO metadata (version) VALUES (5);''') + + self.execute('''CREATE TABLE IF NOT EXISTS users ( + user_id text PRIMARY KEY, + name text, + given_name text, + family_name text, + picture text, + locale text, + ace_theme text NOT NULL DEFAULT "tomorrow");''') + + self.execute('''CREATE TABLE IF NOT EXISTS permissions ( + user_id text NOT NULL, + role text NOT NULL, + course_id integer, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE + );''') + + self.execute('''CREATE TABLE IF NOT EXISTS course_registration ( + user_id text NOT NULL, + course_id integer NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (course_id) REFERENCES courses (course_id) ON DELETE CASCADE ON UPDATE CASCADE + );''') + + self.execute('''CREATE TABLE IF NOT EXISTS courses ( + course_id integer PRIMARY KEY AUTOINCREMENT, + title text NOT NULL UNIQUE, + introduction text, + visible integer NOT NULL, + passcode text, + date_created timestamp NOT NULL, + date_updated timestamp NOT NULL + );''') + + self.execute('''CREATE TABLE IF NOT EXISTS assignments ( + course_id integer NOT NULL, + assignment_id integer PRIMARY KEY AUTOINCREMENT, + title text NOT NULL, + introduction text, + visible integer NOT NULL, + start_date timestamp, + due_date timestamp, + allow_late integer, + late_percent real, + view_answer_late integer, + has_timer int NOT NULL, + hour_timer int, + minute_timer int, + date_created timestamp NOT NULL, + date_updated timestamp NOT NULL, + FOREIGN KEY (course_id) REFERENCES courses (course_id) ON DELETE CASCADE ON UPDATE CASCADE + );''') + + self.execute('''CREATE TABLE IF NOT EXISTS problems ( + course_id integer NOT NULL, + assignment_id integer NOT NULL, + problem_id integer PRIMARY KEY AUTOINCREMENT, + title text NOT NULL, + visible integer NOT NULL, + answer_code text NOT NULL, + answer_description text, + hint text, + max_submissions integer NOT NULL, + credit text, + data_url text, + data_file_name text, + data_contents text, + back_end text NOT NULL, + expected_text_output text NOT NULL, + expected_image_output text NOT NULL, + instructions text NOT NULL, + output_type text NOT NULL, + show_answer integer NOT NULL, + show_student_submissions integer NOT NULL, + show_expected integer NOT NULL, + show_test_code integer NOT NULL, + test_code text, + date_created timestamp NOT NULL, + date_updated timestamp NOT NULL, + FOREIGN KEY (course_id) REFERENCES courses (course_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (assignment_id) REFERENCES assignments (assignment_id) ON DELETE CASCADE ON UPDATE CASCADE + );''') + + self.execute('''CREATE TABLE IF NOT EXISTS submissions ( + course_id integer NOT NULL, + assignment_id integer NOT NULL, + problem_id integer NOT NULL, + user_id text NOT NULL, + submission_id integer NOT NULL, + code text NOT NULL, + text_output text NOT NULL, + image_output text NOT NULL, + passed integer NOT NULL, + date timestamp NOT NULL, + FOREIGN KEY (course_id) REFERENCES courses (course_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (assignment_id) REFERENCES assignments (assignment_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (problem_id) REFERENCES problems (problem_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (course_id, assignment_id, problem_id, user_id, submission_id) + );''') + + self.execute('''CREATE TABLE IF NOT EXISTS scores ( + course_id integer NOT NULL, + assignment_id integer NOT NULL, + problem_id integer NOT NULL, + user_id text NOT NULL, + score real NOT NULL, + FOREIGN KEY (course_id) REFERENCES courses (course_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (assignment_id) REFERENCES assignments (assignment_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (problem_id) REFERENCES problems (problem_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (course_id, assignment_id, problem_id, user_id) + );''') + + self.execute('''CREATE TABLE IF NOT EXISTS user_assignment_start ( + user_id text NOT NULL, + course_id text NOT NULL, + assignment_id text NOT NULL, + start_time timestamp NOT NULL, + FOREIGN KEY (course_id) REFERENCES courses (course_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (assignment_id) REFERENCES assignments (assignment_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE + );''') + + def get_database_version(self): + sql = '''SELECT MAX(version) + FROM metadata''' + + return self.fetchone(sql)[0] + + def update_database_version(self, version): + sql = '''DELETE FROM metadata''' + self.execute(sql) + + sql = '''INSERT INTO metadata (version) + VALUES (?)''' + self.execute(sql, (version,)) + + def set_user_assignment_start_time(self, course_id, assignment_id, user_id, start_time): + start_time = datetime.strptime(start_time, "%a, %d %b %Y %H:%M:%S %Z") + + sql = '''INSERT INTO user_assignment_starts (course_id, assignment_id, user_id, start_time) + VALUES (?, ?, ?, ?)''' + + self.execute(sql, (course_id, assignment_id, user_id, start_time,)) + + def get_user_assignment_start_time(self, course_id, assignment_id, user_id): + sql = '''SELECT start_time + FROM user_assignment_starts + WHERE course_id = ? + AND assignment_id = ? + AND user_id = ?''' + + row = self.fetchone(sql, (course_id, assignment_id, user_id,)) + if row: + return row["start_time"].strftime("%a, %d %b %Y %H:%M:%S %Z") + + def get_all_user_assignment_start_times(self, course_id, assignment_id): + start_times = {} + + sql = '''SELECT user_id, start_time + FROM user_assignment_starts + WHERE course_id = ? + AND assignment_id = ?''' + + for row in self.fetchall(sql, (course_id, assignment_id,)): + start_time = datetime.strftime(row["start_time"], "%a, %d %b %Y %H:%M:%S ") + timer_ended = self.has_user_assignment_start_timer_ended(course_id, assignment_id, start_time) + time_info = {"start_time": row["start_time"], "timer_ended": timer_ended} + start_times[row["user_id"]] = time_info + + return start_times + + def has_user_assignment_start_timer_ended(self, course_id, assignment_id, start_time): + if not start_time: + return False + + curr_time = datetime.now() + start_time = datetime.strptime(start_time, "%a, %d %b %Y %H:%M:%S ") + + sql = '''SELECT hour_timer, minute_timer + FROM assignments + WHERE course_id = ? + AND assignment_id = ?''' + row = self.fetchone(sql, (course_id, assignment_id,)) + + if row: + elapsed_time = curr_time - start_time + seconds = elapsed_time.total_seconds() + e_hours = math.floor(seconds/3600) + e_minutes = math.floor((seconds/60) - (e_hours*60)) + e_seconds = (seconds - (e_minutes*60) - (e_hours*3600)) + + if e_hours > int(row["hour_timer"]): + return True + elif e_hours == int(row["hour_timer"]) and e_minutes > int(row["minute_timer"]): + return True + elif e_hours == int(row["hour_timer"]) and e_minutes == int(row["minute_timer"]) and e_seconds > 0: + return True + + return False + + def reset_user_assignment_start_timer(self, course_id, assignment_id, user_id): + sql = '''DELETE FROM user_assignment_starts + WHERE course_id = ? + AND assignment_id = ? + AND user_id = ?''' + + self.execute(sql, (course_id, assignment_id, user_id)) + + def user_exists(self, user_id): + sql = '''SELECT user_id + FROM users + WHERE user_id = ?''' + + return self.fetchone(sql, (user_id,)) != None + + def administrator_exists(self): + sql = '''SELECT COUNT(*) AS num_administrators + FROM permissions + WHERE role = "administrator"''' + + return self.fetchone(sql)["num_administrators"] + + def is_administrator(self, user_id): + return self.user_has_role(user_id, 0, "administrator") + + def user_has_role(self, user_id, course_id, role): + sql = '''SELECT COUNT(*) AS has_role + FROM permissions + WHERE role = ? + AND user_id = ? + AND course_id = ?''' + + return self.fetchone(sql, (role, user_id, course_id, ))["has_role"] > 0 + + def get_courses_with_role(self, user_id, role): + sql = '''SELECT course_id + FROM permissions + WHERE user_id = ? + AND role = ?''' + + course_ids = set() + for row in self.fetchall(sql, (user_id, role, )): + course_ids.add(row["course_id"]) + + return course_ids + + def get_users_from_role(self, course_id, role): + sql = '''SELECT user_id + FROM permissions + WHERE role = ? + AND (course_id = ? OR course_id IS NULL)''' + + rows = self.fetchall(sql, (role, course_id,)) + return [row["user_id"] for row in rows] + + def get_course_id_from_role(self, user_id): + sql = '''SELECT course_id + FROM permissions + WHERE user_id = ?''' + + row = self.fetchone(sql, (user_id,)) + + if row: + return row["course_id"] + else: + return -1 # The user is a student. + + def set_user_dict_defaults(self, user_dict): + if "name" not in user_dict: + user_dict["name"] = "[Unknown name]" + if "given_name" not in user_dict: + user_dict["given_name"] = "[Unknown given name]" + if "family_name" not in user_dict: + user_dict["family_name"] = "[Unknown family name]" + if "locale" not in user_dict: + user_dict["locale"] = "" + + def add_user(self, user_id, user_dict): + self.set_user_dict_defaults(user_dict) + + sql = '''INSERT INTO users (user_id, name, given_name, family_name, locale, ace_theme) + VALUES (?, ?, ?, ?, ?, ?)''' + + self.execute(sql, (user_id, user_dict["name"], user_dict["given_name"], user_dict["family_name"], user_dict["locale"], "tomorrow")) + + def register_user_for_course(self, course_id, user_id): + sql = '''INSERT INTO course_registrations (course_id, user_id) + VALUES (?, ?)''' + + self.execute(sql, (course_id, user_id,)) + + def unregister_user_from_course(self, course_id, user_id): + self.execute('''DELETE FROM course_registrations + WHERE course_id = ? + AND user_id = ?''', (course_id, user_id, )) + + self.execute('''DELETE FROM scores + WHERE course_id = ? + AND user_id = ?''', (course_id, user_id, )) + + self.execute('''DELETE FROM submissions + WHERE course_id = ? + AND user_id = ?''', (course_id, user_id, )) + + self.execute('''DELETE FROM user_assignment_starts + WHERE course_id = ? + AND user_id = ?''', (course_id, user_id, )) + + def check_user_registered(self, course_id, user_id): + sql = '''SELECT 1 + FROM course_registrations + WHERE course_id = ? + AND user_id = ?''' + + if self.fetchone(sql, (course_id, user_id,)): + return True + + return False + + def get_user_info(self, user_id): + sql = '''SELECT * + FROM users + WHERE user_id = ?''' + + user = self.fetchone(sql, (user_id,)) + user_info = {"enable_vim": user["enable_vim"], "user_id": user_id, "name": user["name"], "given_name": user["given_name"], "family_name": user["family_name"], + "locale": user["locale"], "ace_theme": user["ace_theme"], "use_auto_complete": user["use_auto_complete"]} + + return user_info + + def add_permissions(self, course_id, user_id, role): + sql = '''SELECT role + FROM permissions + WHERE user_id = ? + AND (course_id = ? OR course_id IS NULL)''' + + # Admins are not assigned to a particular course. + if not course_id: + course_id = 0 + + role_exists = self.fetchone(sql, (user_id, int(course_id),)) != None + + if role_exists: + sql = '''UPDATE permissions + SET role = ?, course_id = ? + WHERE user_id = ?''' + + self.execute(sql, (role, course_id, user_id,)) + else: + sql = '''INSERT INTO permissions (user_id, role, course_id) + VALUES (?, ?, ?)''' + + self.execute(sql, (user_id, role, course_id,)) + + def remove_permissions(self, course_id, user_id, role): + sql = '''DELETE FROM permissions + WHERE user_id = ? + AND role = ? + AND (course_id = ? OR course_id IS NULL)''' + + # Admins are not assigned to a particular course. + if not course_id: + course_id = "0" + + self.execute(sql, (user_id, role, int(course_id),)) + + def add_admin_permissions(self, user_id): + self.add_permissions(None, user_id, "administrator") + + def get_user_count(self): + sql = '''SELECT COUNT(*) AS count + FROM users''' + + return self.fetchone(sql)["count"] + + def course_exists(self, course_id): + sql = '''SELECT COUNT(*) AS count + FROM courses + WHERE course_id = ?''' + + if self.fetchone(sql, (course_id,)): + return True + else: + return False + + def get_courses_connected_to_user(self, user_id): + courses = [] + sql = '''SELECT p.course_id, c.title + FROM permissions p + INNER JOIN courses c + ON p.course_id = c.course_id + WHERE user_id = ?''' + + for course in self.fetchall(sql, (user_id,)): + course_basics = {"id": course["course_id"], "title": course["title"]} + courses.append([course["course_id"], course_basics]) + return courses + + def get_course_ids(self): + sql = '''SELECT course_id + FROM courses''' + + return [course[0] for course in self.fetchall(sql)] + + def get_assignment_ids(self, course_id): + if not course_id: + return [] + + sql = '''SELECT assignment_id + FROM assignments + WHERE course_id = ?''' + + return [assignment[0] for assignment in self.fetchall(sql, (int(course_id),))] + + def get_exercise_ids(self, course_id, assignment_id): + if not assignment_id: + return [] + + sql = '''SELECT exercise_id + FROM exercises + WHERE course_id = ? + AND assignment_id = ?''' + + return [exercise[0] for exercise in self.fetchall(sql, (int(course_id), int(assignment_id),))] + + def get_courses(self, show_hidden=True): + courses = [] + + sql = '''SELECT course_id, title, visible, introduction, consent_text + FROM courses + ORDER BY title''' + + for course in self.fetchall(sql): + if course["visible"] or show_hidden: + course_basics = {"id": course["course_id"], "title": course["title"], "visible": course["visible"], "introduction": course["introduction"], "exists": True, "consent_text": course["consent_text"]} + course_basics["consent_text"] = convert_markdown_to_html(course_basics["consent_text"]) + courses.append([course["course_id"], course_basics]) + + return courses + + def get_assignments(self, course_id, show_hidden=True): + assignments = [] + + sql = '''SELECT c.course_id, c.title as course_title, c.visible as course_visible, a.assignment_id, + a.title as assignment_title, a.visible as assignment_visible, a.start_date, a.due_date + FROM assignments a + INNER JOIN courses c + ON c.course_id = a.course_id + WHERE c.course_id = ? + ORDER BY a.title''' + + for row in self.fetchall(sql, (course_id,)): + if row["assignment_visible"] or show_hidden: + course_basics = {"id": row["course_id"], "title": row["course_title"], "visible": bool(row["course_visible"]), "exists": True} + assignment_basics = {"id": row["assignment_id"], "title": row["assignment_title"], "visible": row["assignment_visible"], "start_date": row["start_date"], "due_date": row["due_date"], "exists": False, "course": course_basics} + assignments.append([row["assignment_id"], assignment_basics]) + + return assignments + + def get_exercises(self, course_id, assignment_id, show_hidden=True): + exercises = [] + + sql = '''SELECT exercise_id, title, visible, enable_pair_programming + FROM exercises + WHERE course_id = ? + AND assignment_id = ? + ORDER BY title''' + + for exercise in self.fetchall(sql, (course_id, assignment_id,)): + if exercise["visible"] or show_hidden: + assignment_basics = self.get_assignment_basics(course_id, assignment_id) + exercise_basics = {"enable_pair_programming": exercise["enable_pair_programming"], "id": exercise["exercise_id"], "title": exercise["title"], "visible": exercise["visible"], "exists": True, "assignment": assignment_basics} + exercises.append([exercise["exercise_id"], exercise_basics, course_id, assignment_id]) + + return exercises + + def get_available_courses(self, user_id): + available_courses = [] + + sql = '''SELECT course_id, title, introduction, passcode, consent_text, consent_alternative_text + FROM courses + WHERE course_id NOT IN + ( + SELECT course_id + FROM course_registrations + WHERE user_id = ? + ) + AND visible = 1 + ORDER BY title''' + + for course in self.fetchall(sql, (user_id,)): + course_basics = {"id": course["course_id"], "title": course["title"], "introduction": course["introduction"], "passcode": course["passcode"], "consent_text": course["consent_text"], "consent_alternative_text": course["consent_alternative_text"]} + course_basics["consent_text"] = convert_markdown_to_html(course_basics["consent_text"]) + course_basics["consent_alternative_text"] = convert_markdown_to_html(course_basics["consent_alternative_text"]) + available_courses.append([course["course_id"], course_basics]) + + return available_courses + + def get_registered_courses(self, user_id): + registered_courses = [] + + sql = '''SELECT r.course_id, c.title, c.consent_text + FROM course_registrations r + INNER JOIN courses c + ON r.course_id = c.course_id + WHERE r.user_id = ? + AND c.visible = 1''' + + for course in self.fetchall(sql, (user_id,)): + course_basics = {"id": course["course_id"], "title": course["title"], "consent_text": course["consent_text"]} + course_basics["consent_text"] = convert_markdown_to_html(course_basics["consent_text"]) + registered_courses.append([course["course_id"], course_basics]) + + return registered_courses + + def get_partner_info(self, course, user_id): + # Gets list of users. + users = [x[1] for x in self.get_registered_students(course) if not x[0] == user_id] + + # Adds users to dict to find duplicate names. + user_duplicates_dict = {} + for user in users: + if user["name"] in user_duplicates_dict.keys(): + user_duplicates_dict[user["name"]].append(user["id"]) + else: + user_duplicates_dict[user["name"]] = [user["id"]] + + # Adds all users to a dictionary with name (and obscured email if applicable) as key and id as value. + user_dict = {} + for user in user_duplicates_dict: + if len(user_duplicates_dict[user]) > 1: + for id in user_duplicates_dict[user]: + user_dict[user + " — " + self.obscure_email(id, user_duplicates_dict[user])] = id + else: + user_dict[user] = user_duplicates_dict[user][0] + + return user_dict + + def obscure_email(self, full_email, all_emails): + email = full_email.split("@")[0] if "@" in full_email else full_email + email_end = full_email.split("@")[1] if "@" in full_email else full_email + + temp_email = email[0] + for other_email in all_emails: + other_email = other_email.split("@")[0] if "@" in other_email else other_email + if other_email == email: + pass + else: + for i in range(len(temp_email), len(min(email, other_email))): + if temp_email == other_email[:i]: + temp_email = temp_email + email[i] + else: + break + + # Obscures all but essential characters of email. + return temp_email + (("*")*(len(email)-len(temp_email))) + "@" + email_end + + def get_registered_students(self, course_id): + registered_students = [] + + sql = '''SELECT r.user_id, u.name + FROM course_registrations r + INNER JOIN users u + ON r.user_id = u.user_id + WHERE r.course_id = ?''' + + for student in self.fetchall(sql, (course_id,)): + student_info = {"id": student["user_id"], "name": student["name"]} + registered_students.append([student["user_id"], student_info]) + + return registered_students + + # Gets whether or not a student has passed each assignment in the course. + def get_assignment_statuses(self, course_id, user_id): + sql = '''SELECT assignment_id, + title, + start_date, + due_date, + SUM(passed) AS num_passed, + SUM(passed) = COUNT(assignment_id) AS passed_all, + (SUM(passed) > 0 OR num_submissions > 0) AND SUM(passed) < COUNT(assignment_id) AS in_progress, + has_timer, + hour_timer, + minute_timer + FROM ( + SELECT a.assignment_id, + a.title, + a.start_date, + a.due_date, + IFNULL(MAX(s.passed), 0) AS passed, + COUNT(s.submission_id) AS num_submissions, + a.has_timer, + a.hour_timer, + a.minute_timer + FROM exercises e + LEFT JOIN submissions s + ON e.course_id = s.course_id + AND e.assignment_id = s.assignment_id + AND e.exercise_id = s.exercise_id + AND (s.user_id = ? OR s.user_id IS NULL) + INNER JOIN assignments a + ON e.course_id = a.course_id + AND e.assignment_id = a.assignment_id + WHERE e.course_id = ? + AND a.visible = 1 + AND e.visible = 1 + GROUP BY e.assignment_id, e.exercise_id + ) + GROUP BY assignment_id, title + ORDER BY title''' + + assignment_statuses = [] + for row in self.fetchall(sql, (user_id, int(course_id),)): + assignment_dict = {"id": row["assignment_id"], "title": row["title"], "start_date": row["start_date"], "due_date": row["due_date"], "passed": row["passed_all"], "in_progress": row["in_progress"], "num_passed": row["num_passed"], "num_exercises": row["num_exercises"], "has_timer": row["has_timer"], "hour_timer": row["hour_timer"], "minute_timer": row["minute_timer"]} + assignment_statuses.append([row["assignment_id"], assignment_dict]) + + return assignment_statuses + + # Gets the number of submissions a student has made for each exercise + # in an assignment and whether or not they have passed the exercise. + def get_exercise_statuses(self, course_id, assignment_id, user_id, show_hidden=True): + # This happens when you are creating a new assignment. + if not assignment_id: + return [] + + sql = '''SELECT e.exercise_id, + e.title, + e.enable_pair_programming, + IFNULL(MAX(s.passed), 0) AS passed, + COUNT(s.submission_id) AS num_submissions, + COUNT(s.submission_id) > 0 AND IFNULL(MAX(s.passed), 0) = 0 AS in_progress, + IFNULL(sc.score, 0) as score + FROM exercises e + LEFT JOIN submissions s + ON e.course_id = s.course_id + AND e.assignment_id = s.assignment_id + AND e.exercise_id = s.exercise_id + AND s.user_id = ? + LEFT JOIN scores sc + ON e.course_id = sc.course_id + AND e.assignment_id = sc.assignment_id + AND e.exercise_id = sc.exercise_id + AND (sc.user_id = ? OR sc.user_id IS NULL) + WHERE e.course_id = ? + AND e.assignment_id = ? + AND e.visible = 1 + GROUP BY e.assignment_id, e.exercise_id + ORDER BY e.title''' + + exercise_statuses = [] + for row in self.fetchall(sql, (user_id, user_id, int(course_id), int(assignment_id),)): + exercise_dict = {"enable_pair_programming": row["enable_pair_programming"], "id": row["exercise_id"], "title": row["title"], "passed": row["passed"], "num_submissions": row["num_submissions"], "in_progress": row["in_progress"], "score": row["score"]} + exercise_statuses.append([row["exercise_id"], exercise_dict]) + + return exercise_statuses + + ## Calculates the average score across all students for each assignment in a course, + ## as well as the number of students who have completed each assignment. + def get_course_scores(self, course_id, assignments): + course_scores = {} + + sql = '''SELECT COUNT(*) AS num_registered_students + FROM course_registrations + WHERE course_id = ?''' + num_students = self.fetchone(sql, (course_id, ))["num_registered_students"] + + sql = ''' + WITH + assignment_info AS ( + SELECT assignment_id, COUNT(*) * 100.0 AS points_possible + FROM exercises + WHERE course_id = ? + AND visible = 1 + /*AND assignment_id IN (SELECT assignment_id FROM assignments WHERE visible = 1)*/ + GROUP BY assignment_id + ), + + assignment_score_info AS ( + SELECT assignment_id, SUM(score) AS score + FROM scores + WHERE course_id = ? + GROUP BY assignment_id, user_id + ), + + exercise_info AS ( + SELECT + a.assignment_id, + (score / points_possible) * 100.0 AS percentage, + points_possible = score AS completed + FROM assignment_info a + INNER JOIN assignment_score_info s + ON a.assignment_id = s.assignment_id + ) + + SELECT e.assignment_id, + SUM(e.completed) AS num_students_completed, + SUM(e.percentage) AS total_percentage + FROM exercise_info e + GROUP BY e.assignment_id''' + +# for row in self.fetchall(sql, (course_id, course_id, )): +# x = 1 +# assignment_dict = {"assignment_id": row["assignment_id"], +# "num_students_completed": row["num_students_completed"], +# "num_students": num_students, +# "avg_score": "{:.1f}".format(row["total_percentage"] / num_students)} +# +# course_scores[row["assignment_id"]] = assignment_dict + + for assignment in assignments: + if assignment[0] not in course_scores: + course_scores[assignment[0]] = {"assignment_id": assignment[0], + "num_students_completed": 0, + "num_students": num_students, + "avg_score": "0.0"} + + return course_scores + + # Gets all users who have submitted on a particular assignment + # and creates a list of their average scores for the assignment. + def get_assignment_scores(self, course_id, assignment_id): + scores = [] + + sql = '''SELECT u.name, s.user_id, (SUM(s.score) / b.num_exercises) AS percent_passed + FROM scores s + INNER JOIN users u + ON s.user_id = u.user_id + INNER JOIN ( + SELECT COUNT(DISTINCT exercise_id) AS num_exercises + FROM exercises + WHERE course_id = ? + AND assignment_id = ? + AND visible = 1 + ) b + WHERE s.course_id = ? + AND s.assignment_id = ? + AND s.user_id NOT IN + ( + SELECT user_id + FROM permissions + WHERE course_id = 0 OR course_id = ? + ) + AND s.exercise_id NOT IN + ( + SELECT exercise_id + FROM exercises + WHERE course_id = ? + AND assignment_id = ? + AND visible = 0 + ) + GROUP BY s.course_id, s.assignment_id, s.user_id + ORDER BY u.family_name, u.given_name''' + + for user in self.fetchall(sql, (int(course_id), int(assignment_id), int(course_id), int(assignment_id), int(course_id), int(course_id), int(assignment_id),)): + scores_dict = {"name": user["name"], "user_id": user["user_id"], "percent_passed": user["percent_passed"]} + scores.append([user["user_id"], scores_dict]) + + return scores + + def get_exercise_scores(self, course_id, assignment_id, exercise_id): + scores = [] + + sql = '''SELECT u.name, s.user_id, sc.score, COUNT(s.submission_id) AS num_submissions + FROM submissions s + INNER JOIN users u + ON u.user_id = s.user_id + INNER JOIN scores sc + ON sc.course_id = s.course_id + AND sc.assignment_id = s.assignment_id + AND sc.exercise_id = s.exercise_id + AND sc.user_id = s.user_id + WHERE s.course_id = ? + AND s.assignment_id = ? + AND s.exercise_id = ? + GROUP BY s.user_id + ORDER BY u.family_name, u.given_name''' + + for user in self.fetchall(sql, (int(course_id), int(assignment_id), int(exercise_id),)): + scores_dict = {"name": user["name"], "user_id": user["user_id"], "num_submissions": user["num_submissions"], "score": user["score"]} + scores.append([user["user_id"], scores_dict]) + + return scores + + def get_exercise_score(self, course_id, assignment_id, exercise_id, user_id): + sql = '''SELECT score + FROM scores + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ?''' + + row = self.fetchone(sql, (int(course_id), int(assignment_id), int(exercise_id), user_id,)) + if row: + return row["score"] + + def calc_exercise_score(self, assignment_details, passed): + score = 0 + if passed: + if assignment_details["due_date"] and assignment_details["due_date"] < datetime.now(): + if assignment_details["allow_late"]: + score = 100 * assignment_details["late_percent"] + else: + score = 100 + + return score + + def save_exercise_score(self, course_id, assignment_id, exercise_id, user_id, new_score): + score = self.get_exercise_score(course_id, assignment_id, exercise_id, user_id) + + if score != None: + sql = '''UPDATE scores + SET score = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ?''' + + self.execute(sql, (new_score, course_id, assignment_id, exercise_id, user_id)) + + else: + sql = '''INSERT INTO scores (course_id, assignment_id, exercise_id, user_id, score) + VALUES (?, ?, ?, ?, ?)''' + + self.execute(sql, (course_id, assignment_id, exercise_id, user_id, new_score)) + + def get_submissions_basic(self, course_id, assignment_id, exercise_id, user_id): + submissions = [] + sql = '''SELECT s.submission_id, s.date, s.passed, u.name + FROM submissions s + LEFT JOIN users u + ON s.partner_id = u.user_id + WHERE s.course_id = ? + AND s.assignment_id = ? + AND s.exercise_id = ? + AND s.user_id = ? + ORDER BY s.submission_id DESC''' + + for submission in self.fetchall(sql, (int(course_id), int(assignment_id), int(exercise_id), user_id,)): + submissions.append([submission["submission_id"], submission["date"].strftime("%a, %d %b %Y %H:%M:%S UTC"), submission["passed"], submission["name"]]) + return submissions + + def get_tests(self, course_id, assignment_id, exercise_id): + if not exercise_id: + return [] + + tests = [] + sql = '''SELECT code, test_instructions, text_output, image_output + FROM tests + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''' + + for test in self.fetchall(sql, (int(course_id), int(assignment_id), int(exercise_id))): + tests.append({"code": test["code"], "test_instructions": test["test_instructions"], "text_output": test["text_output"], "image_output": test["image_output"]}) + return tests + + def get_student_submissions(self, course_id, assignment_id, exercise_id, user_id): + student_submissions = [] + index = 1 + + sql = '''SELECT DISTINCT code + FROM submissions + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND passed = 1 + AND user_id != ? + GROUP BY user_id + ORDER BY date''' + + for submission in self.fetchall(sql, (course_id, assignment_id, exercise_id, user_id,)): + student_submissions.append([index, submission["code"]]) + index += 1 + return student_submissions + + def get_help_requests(self, course_id): + help_requests = [] + + sql = '''SELECT r.course_id, a.assignment_id, e.exercise_id, c.title as course_title, a.title as assignment_title, e.title as exercise_title, r.user_id, u.name, r.code, r.text_output, r.image_output, r.student_comment, r.suggestion, r.approved, r.suggester_id, r.approver_id, r.date, r.more_info_needed + FROM help_requests r + INNER JOIN users u + ON r.user_id = u.user_id + INNER JOIN courses c + ON r.course_id = c.course_id + INNER JOIN assignments a + ON r.assignment_id = a.assignment_id + INNER JOIN exercises e + ON r.exercise_id = e.exercise_id + WHERE r.course_id = ? + ORDER BY r.date DESC''' + + for request in self.fetchall(sql, (course_id,)): + help_requests.append({"course_id": request["course_id"], "assignment_id": request["assignment_id"], "exercise_id": request["exercise_id"], "course_title": request["course_title"], "assignment_title": request["assignment_title"], "exercise_title": request["exercise_title"], "user_id": request["user_id"], "name": request["name"], "code": request["code"], "text_output": request["text_output"], "image_output": request["image_output"], "student_comment": request["student_comment"], "suggestion": request["suggestion"], "approved": request["approved"], "suggester_id": request["suggester_id"], "approver_id": request["approver_id"], "date": request["date"], "more_info_needed": request["more_info_needed"]}) + return help_requests + + def get_student_help_requests(self, user_id): + help_requests = [] + + sql = '''SELECT r.course_id, a.assignment_id, e.exercise_id, c.title as course_title, a.title as assignment_title, e.title as exercise_title, r.user_id, u.name, r.code, r.text_output, r.image_output, r.student_comment, r.suggestion, r.approved, r.suggester_id, r.approver_id, r.more_info_needed + FROM help_requests r + INNER JOIN users u + ON r.user_id = u.user_id + INNER JOIN courses c + ON r.course_id = c.course_id + INNER JOIN assignments a + ON r.assignment_id = a.assignment_id + INNER JOIN exercises e + ON r.exercise_id = e.exercise_id + WHERE r.user_id = ? + ORDER BY r.date DESC''' + + for request in self.fetchall(sql, (user_id,)): + help_requests.append({"course_id": request["course_id"], "assignment_id": request["assignment_id"], "exercise_id": request["exercise_id"], "course_title": request["course_title"], "assignment_title": request["assignment_title"], "exercise_title": request["exercise_title"], "user_id": request["user_id"], "name": request["name"], "code": request["code"], "text_output": request["text_output"], "image_output": request["text_output"], "image_output": request["image_output"], "student_comment": request["student_comment"], "suggestion": request["suggestion"], "approved": request["approved"], "suggester_id": request["suggester_id"], "approver_id": request["approver_id"], "more_info_needed": request["more_info_needed"]}) + + return help_requests + + def get_exercise_help_requests(self, course_id, assignment_id, exercise_id, user_id): + sql = '''SELECT text_output + FROM help_requests + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ?''' + row = self.fetchone(sql, (course_id, assignment_id, exercise_id, user_id,)) + orig_output = re.sub("#.*", "", row["text_output"]) + + sql = '''SELECT r.course_id, r.assignment_id, r.exercise_id, r.user_id, u.name, r.code, r.text_output, r.image_output, r.student_comment, r.suggestion, r.approved, r.suggester_id, r.approver_id, r.more_info_needed + FROM help_requests r + INNER JOIN users u + ON r.user_id = u.user_id + WHERE r.course_id = ? + AND r.assignment_id = ? + AND r.exercise_id = ? + AND NOT r.user_id = ? + ORDER BY r.date DESC''' + + requests = self.fetchall(sql, (course_id, assignment_id, exercise_id, user_id,)) + + nlp = spacy.load('en_core_web_sm') + orig = nlp(orig_output) + help_requests = [] + + for request in requests: + curr = nlp(re.sub("#.*", "", request["text_output"])) + psim = curr.similarity(orig) + request_info = {"psim": psim, "course_id": request["course_id"], "assignment_id": request["assignment_id"], "exercise_id": request["exercise_id"], "user_id": request["user_id"], "name": request["name"], "code": request["code"], "text_output": request["text_output"], "image_output": request["text_output"], "image_output": request["image_output"], "student_comment": request["student_comment"], "suggestion": request["suggestion"], "approved": request["approved"], "suggester_id": request["suggester_id"], "approver_id": request["approver_id"], "more_info_needed": request["more_info_needed"]} + help_requests.append(request_info) + + return sorted(help_requests, key=lambda x: x["psim"], reverse=True) + + def get_help_request(self, course_id, assignment_id, exercise_id, user_id): + sql = '''SELECT r.user_id, u.name, r.code, r.text_output, r.image_output, r.student_comment, r.suggestion, r.approved, r.suggester_id, r.approver_id, r.more_info_needed + FROM help_requests r + INNER JOIN users u + ON r.user_id = u.user_id + WHERE r.course_id = ? + AND r.assignment_id = ? + AND r.exercise_id = ? + AND r.user_id = ?''' + + request = self.fetchone(sql, (course_id, assignment_id, exercise_id, user_id,)) + if request: + help_request = {"course_id": course_id, "assignment_id": assignment_id, "exercise_id": exercise_id, "user_id": request["user_id"], "name": request["name"], "code": request["code"], "text_output": request["text_output"], "image_output": request["image_output"], "student_comment": request["student_comment"], "approved": request["approved"], "suggester_id": request["suggester_id"], "approver_id": request["approver_id"], "more_info_needed": request["more_info_needed"]} + if request["suggestion"]: + help_request["suggestion"] = request["suggestion"] + else: + help_request["suggestion"] = None + + return help_request + + def compare_help_requests(self, course_id, assignment_id, exercise_id, user_id): + # Get the original help request, including its output type + sql = '''SELECT r.text_output, e.expected_text_output, r.image_output, e.expected_image_output, e.output_type + FROM help_requests r + INNER JOIN exercises e + ON e.course_id = r.course_id + AND e.assignment_id = r.assignment_id + AND e.exercise_id = r.exercise_id + WHERE r.course_id = ? + AND r.assignment_id = ? + AND r.exercise_id = ? + AND r.user_id = ?''' + row = self.fetchone(sql, (course_id, assignment_id, exercise_id, user_id,)) + + #the original output type will be either txt or jpg depending on the output type of the exercise + orig_output = None + + if row["output_type"] == "jpg": + if row["image_output"] != row["expected_image_output"]: + orig_output = row["image_output"] + + else: + if row["text_output"] != row["expected_text_output"]: + orig_output = row["text_output"] + + #get all other help requests in the course that have the same output type + if orig_output: + sql = '''SELECT r.course_id, r.assignment_id, r.exercise_id, r.user_id, u.name, r.code, r.text_output, r.image_output, r.student_comment, r.suggestion + FROM help_requests r + INNER JOIN users u + ON r.user_id = u.user_id + INNER JOIN exercises e + ON e.course_id = r.course_id + AND e.assignment_id = r.assignment_id + AND e.exercise_id = r.exercise_id + WHERE r.course_id = ? + AND NOT r.user_id = ? + AND e.output_type = ?''' + requests = self.fetchall(sql, (course_id, user_id, row["output_type"])) + sim_dict = [] + + #jpg output uses the diff_jpg function in helper.py, txt output uses .similarity() from the Spacy module + if row["output_type"] == "jpg": + for request in requests: + diff_image, diff_percent = diff_jpg(orig_output, request["image_output"]) + if diff_percent < .10: + request_info = {"psim": 1 - diff_percent, "course_id": request["course_id"], "assignment_id": request["assignment_id"], "exercise_id": request["exercise_id"], "user_id": request["user_id"], "name": request["name"], "student_comment": request["student_comment"], "code": request["code"], "text_output": request["text_output"], "suggestion": request["suggestion"]} + sim_dict.append(request_info) + + else: + nlp = spacy.load('en_core_web_sm') + orig = nlp(orig_output) + + for request in requests: + curr = nlp(request["text_output"]) + psim = curr.similarity(orig) + sim = False + + #these thresholds can be changed in the future + if len(orig) < 10 and len(curr) < 10: + if psim > .30: + sim = True + elif len(orig) < 100 and len(curr) < 100: + if psim > .50: + sim = True + elif len(orig) < 200 and len(curr) < 200: + if psim > .70: + sim = True + else: + if psim > .90: + sim = True + + if sim: + request_info = {"psim": psim, "course_id": request["course_id"], "assignment_id": request["assignment_id"], "exercise_id": request["exercise_id"], "user_id": request["user_id"], "name": request["name"], "student_comment": request["student_comment"], "code": request["code"], "text_output": request["text_output"], "suggestion": request["suggestion"]} + sim_dict.append(request_info) + + return sim_dict + + def get_same_suggestion(self, help_request): + sql = '''SELECT r.suggestion, e.output_type, r.text_output, r.image_output + FROM help_requests r + INNER JOIN exercises e + ON e.exercise_id = r.exercise_id + WHERE r.course_id = ? + AND r.suggestion NOT NULL + AND e.output_type = ( + SELECT output_type + FROM exercises + WHERE exercise_id = ? + )''' + + matches = self.fetchall(sql, (help_request["course_id"], help_request["exercise_id"])) + for match in matches: + if match["output_type"] == "jpg": + if match["image_output"] == help_request["image_output"]: + return match["suggestion"] + else: + if match["text_output"] == help_request["text_output"]: + return match["suggestion"] + + def get_exercise_submissions(self, course_id, assignment_id, exercise_id): + exercise_submissions = [] + + sql = '''SELECT MAX(s.date), s.code, u.user_id, u.name, sc.score, s.passed, p.name AS partner_name + FROM submissions s + LEFT JOIN users p + ON s.partner_id = p.user_id + INNER JOIN users u + ON s.user_id = u.user_id + INNER JOIN scores sc + ON s.user_id = sc.user_id + AND s.course_id = sc.course_id + AND s.assignment_id = sc.assignment_id + AND s.exercise_id = sc.exercise_id + WHERE s.course_id = ? + AND s.assignment_id = ? + AND s.exercise_id = ? + AND s.user_id IN + ( + SELECT user_id + FROM course_registrations + WHERE course_id = ? + ) + GROUP BY s.user_id + ORDER BY u.family_name, u.given_name''' + + for submission in self.fetchall(sql, (course_id, assignment_id, exercise_id, course_id,)): + submission_info = {"user_id": submission["user_id"], "name": submission["name"], "code": submission["code"], "score": submission["score"], "passed": submission["passed"], "partner_name": submission["partner_name"]} + exercise_submissions.append([submission["user_id"], submission_info]) + + return exercise_submissions + + def specify_course_basics(self, course_basics, title, visible): + course_basics["title"] = title + course_basics["visible"] = visible + + def specify_course_details(self, course_details, introduction, passcode, consent_text, consent_alternative_text, date_created, date_updated): + course_details["introduction"] = introduction + course_details["passcode"] = passcode + course_details["date_updated"] = date_updated + course_details["consent_text"] = consent_text + course_details["consent_alternative_text"] = consent_alternative_text + + if course_details["date_created"]: + course_details["date_created"] = date_created + else: + course_details["date_created"] = date_updated + + def specify_assignment_basics(self, assignment_basics, title, visible): + assignment_basics["title"] = title + assignment_basics["visible"] = visible + + def specify_assignment_details(self, assignment_details, introduction, date_created, date_updated, start_date, due_date, allow_late, late_percent, view_answer_late, enable_help_requests, has_timer, hour_timer, minute_timer, allowed_ip_addresses): + assignment_details["introduction"] = introduction + assignment_details["date_updated"] = date_updated + assignment_details["start_date"] = start_date + assignment_details["due_date"] = due_date + assignment_details["allow_late"] = allow_late + assignment_details["late_percent"] = late_percent + assignment_details["view_answer_late"] = view_answer_late + assignment_details["enable_help_requests"] = enable_help_requests + assignment_details["has_timer"] = has_timer + assignment_details["hour_timer"] = hour_timer + assignment_details["minute_timer"] = minute_timer + assignment_details["allowed_ip_addresses"] = allowed_ip_addresses + + if assignment_details["date_created"]: + assignment_details["date_created"] = date_created + else: + assignment_details["date_created"] = date_updated + + def specify_exercise_basics(self, exercise_basics, title, visible): + exercise_basics["title"] = title + exercise_basics["visible"] = visible + + def specify_exercise_details(self, exercise_details, instructions, back_end, output_type, answer_code, answer_description, hint, max_submissions, starter_code, test_code, credit, data_files, show_expected, show_test_code, show_answer, show_student_submissions, expected_text_output, expected_image_output, date_created, date_updated, enable_pair_programming, check_code, tests): + exercise_details["instructions"] = instructions + exercise_details["back_end"] = back_end + exercise_details["output_type"] = output_type + exercise_details["answer_code"] = answer_code + exercise_details["answer_description"] = answer_description + exercise_details["hint"] = hint + exercise_details["max_submissions"] = max_submissions + exercise_details["starter_code"] = starter_code + exercise_details["test_code"] = test_code + exercise_details["credit"] = credit + exercise_details["data_files"] = data_files + exercise_details["show_expected"] = show_expected + exercise_details["show_test_code"] = show_test_code + exercise_details["show_answer"] = show_answer + exercise_details["show_student_submissions"] = show_student_submissions + exercise_details["expected_text_output"] = expected_text_output + exercise_details["expected_image_output"] = expected_image_output + exercise_details["date_updated"] = date_updated + exercise_details["enable_pair_programming"] = enable_pair_programming + exercise_details["check_code"] = check_code + exercise_details["tests"] = tests + + if exercise_details["date_created"]: + exercise_details["date_created"] = date_created + else: + exercise_details["date_created"] = date_updated + + def get_course_basics(self, course_id): + if not course_id: + return {"id": "", "title": "", "visible": True, "exists": False} + + + sql = '''SELECT course_id, title, visible + FROM courses + WHERE course_id = ?''' + + row = self.fetchone(sql, (int(course_id),)) + + return {"id": row["course_id"], "title": row["title"], "visible": bool(row["visible"]), "exists": True} + + def get_assignment_basics(self, course_id, assignment_id): + course_basics = self.get_course_basics(course_id) + + if not assignment_id: + return {"id": "", "title": "", "visible": True, "exists": False, "course": course_basics} + + sql = '''SELECT assignment_id, title, visible + FROM assignments + WHERE course_id = ? + AND assignment_id = ?''' + + row = self.fetchone(sql, (int(course_id), int(assignment_id),)) + if row is None: + return {"id": "", "title": "", "visible": True, "exists": False, "course": course_basics} + else: + return {"id": row["assignment_id"], "title": row["title"], "visible": bool(row["visible"]), "exists": True, "course": course_basics} + + def get_exercise_basics(self, course_id, assignment_id, exercise_id): + assignment_basics = self.get_assignment_basics(course_id, assignment_id) + + if not exercise_id: + return {"id": "", "title": "", "visible": True, "exists": False, "assignment": assignment_basics} + + sql = '''SELECT exercise_id, title, visible, enable_pair_programming + FROM exercises + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''' + + row = self.fetchone(sql, (int(course_id), int(assignment_id), int(exercise_id),)) + if row is None: + return {"enable_pair_programming": False, "id": "", "title": "", "visible": True, "exists": False, "assignment": assignment_basics} + else: + return {"enable_pair_programming": row["enable_pair_programming"], "id": row["exercise_id"], "title": row["title"], "visible": bool(row["visible"]), "exists": True, "assignment": assignment_basics} + + def get_next_prev_exercises(self, course, assignment, exercise, exercises): + prev_exercise = None + next_exercise = None + + if len(exercises) > 0 and exercise: + this_exercise = [i for i in range(len(exercises)) if exercises[i][0] == int(exercise)] + if len(this_exercise) > 0: + this_exercise_index = [i for i in range(len(exercises)) if exercises[i][0] == int(exercise)][0] + + if len(exercises) >= 2 and this_exercise_index != 0: + prev_exercise = exercises[this_exercise_index - 1][1] + + if len(exercises) >= 2 and this_exercise_index != (len(exercises) - 1): + next_exercise = exercises[this_exercise_index + 1][1] + + return {"previous": prev_exercise, "next": next_exercise} + + def get_num_submissions(self, course, assignment, exercise, user): + sql = '''SELECT COUNT(*) + FROM submissions + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ?''' + + return self.fetchone(sql, (int(course), int(assignment), int(exercise), user,))[0] + + def get_next_submission_id(self, course, assignment, exercise, user): + return self.get_num_submissions(course, assignment, exercise, user) + 1 + + def get_last_submission(self, course, assignment, exercise, user): + last_submission_id = self.get_num_submissions(course, assignment, exercise, user) + + if last_submission_id > 0: + return self.get_submission_info(course, assignment, exercise, user, last_submission_id) + + def get_submission_info(self, course, assignment, exercise, user, submission): + sql = '''SELECT code, text_output, image_output, passed, date, partner_id + FROM submissions + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ? + AND submission_id = ?''' + + row = self.fetchone(sql, (int(course), int(assignment), int(exercise), user, int(submission),)) + + test_sql = '''SELECT submission_output_id, text_output, image_output + FROM submission_outputs + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ? + AND submission_id = ?''' + + test_row = self.fetchall(test_sql, (int(course), int(assignment), int(exercise), user, int(submission),)) + + tests = [] + for test in test_row: + tests.append({"test": test["submission_output_id"], "text_output": test["text_output"], "image_output": test["image_output"]}) + + return {"id": submission, "code": row["code"], "text_output": row["text_output"], "image_output": row["image_output"], "passed": row["passed"], "date": row["date"].strftime("%m/%d/%Y, %I:%M:%S %p"), "exists": True, "partner_id": row["partner_id"], "tests": tests} + + def delete_presubmission(self, course, assignment, exercise, user): + sql = '''DELETE FROM presubmissions + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ?''' + + self.execute(sql, (course, assignment, exercise, user)) + + def get_presubmission(self, course, assignment, exercise, user): + sql = '''SELECT code + FROM presubmissions + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ?''' + + row = self.fetchone(sql, (int(course), int(assignment), int(exercise), user)) + + return {"code": row["code"]} if row else None + + def get_course_details(self, course, format_output=False): + if not course: + return {"introduction": "", "passcode": None, "date_created": None, "date_updated": None, "consent_text": "", "consent_alternative_text": ""} + + sql = '''SELECT introduction, passcode, date_created, date_updated, consent_text, consent_alternative_text + FROM courses + WHERE course_id = ?''' + + row = self.fetchone(sql, (int(course),)) + + course_dict = {"introduction": row["introduction"], "passcode": row["passcode"], "date_created": row["date_created"], "date_updated": row["date_updated"], "consent_text": row["consent_text"], "consent_alternative_text": row["consent_alternative_text"]} + if format_output: + course_dict["introduction"] = convert_markdown_to_html(convert_html_to_markdown(course_dict["introduction"])) # Removes html markup from instructions before converting markdown to html + course_dict["consent_text"] = convert_markdown_to_html(course_dict["consent_text"]) + course_dict["consent_alternative_text"] = convert_markdown_to_html(course_dict["consent_alternative_text"]) + + return course_dict + + def get_assignment_details(self, course, assignment, format_output=False): + if not assignment: + return {"introduction": "", "date_created": None, "date_updated": None, "start_date": None, "due_date": None, "allow_late": False, "late_percent": None, "view_answer_late": False, "enable_help_requests": 1, "has_timer": 0, "hour_timer": None, "minute_timer": None, "allowed_ip_addresses": None} + + sql = '''SELECT introduction, date_created, date_updated, start_date, due_date, allow_late, late_percent, view_answer_late, enable_help_requests, allowed_ip_addresses, has_timer, hour_timer, minute_timer + FROM assignments + WHERE course_id = ? + AND assignment_id = ?''' + + row = self.fetchone(sql, (int(course), int(assignment),)) + + assignment_dict = {"introduction": row["introduction"], "date_created": row["date_created"], "date_updated": row["date_updated"], "start_date": row["start_date"], "due_date": row["due_date"], "allow_late": row["allow_late"], "late_percent": row["late_percent"], "view_answer_late": row["view_answer_late"], "allowed_ip_addresses": row["allowed_ip_addresses"], "enable_help_requests": row["enable_help_requests"], "has_timer": row["has_timer"], "hour_timer": row["hour_timer"], "minute_timer": row["minute_timer"]} + + if format_output: + assignment_dict["introduction"] = convert_markdown_to_html(convert_html_to_markdown(assignment_dict["introduction"])) # Removes html markup from instructions before converting markdown to html + + if assignment_dict["allowed_ip_addresses"]: + assignment_dict["allowed_ip_addresses"] = assignment_dict["allowed_ip_addresses"].split(",") + + return assignment_dict + + def get_exercise_details(self, course, assignment, exercise, format_content=False): + if not exercise: + return {"instructions": "", "back_end": "python", "output_type": "txt", "answer_code": "", "answer_description": "", "hint": "", + "max_submissions": 0, "starter_code": "", "test_code": "", "credit": "", "data_files": "", "show_expected": True, "show_test_code": "", "show_answer": True, + "show_student_submissions": False, "expected_text_output": "", "expected_image_output": "", "data_files": "", "date_created": None, "date_updated": None, "enable_pair_programming": False, "check_code": "", "tests": []} + + sql = '''SELECT instructions, back_end, output_type, answer_code, answer_description, hint, max_submissions, starter_code, test_code, credit, data_files, show_expected, show_test_code, show_answer, show_student_submissions, expected_text_output, expected_image_output, data_files, date_created, date_updated, enable_pair_programming, check_code + FROM exercises + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''' + + row = self.fetchone(sql, (int(course), int(assignment), int(exercise),)) + + exercise_dict = {"instructions": row["instructions"], "back_end": row["back_end"], "output_type": row["output_type"], "answer_code": row["answer_code"], "answer_description": row["answer_description"], "hint": row["hint"], "max_submissions": row["max_submissions"], "starter_code": row["starter_code"], "test_code": row["test_code"], "credit": row["credit"], "data_files": row["data_files"].strip(), "show_expected": row["show_expected"], "show_test_code": row["show_test_code"], "show_answer": row["show_answer"], "show_student_submissions": row["show_student_submissions"], "expected_text_output": row["expected_text_output"], "expected_image_output": row["expected_image_output"], "date_created": row["date_created"], "date_updated": row["date_updated"], "enable_pair_programming": row["enable_pair_programming"], "check_code": row["check_code"], "tests": []} + + sql = '''SELECT test_id, code, test_instructions, text_output, image_output + FROM tests + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''' + + tests = self.fetchall(sql, (int(course), int(assignment), int(exercise),)) + + for test in tests: + exercise_dict["tests"].append({"test": test["test_id"], "code": test["code"], "test_instructions": test["test_instructions"], "text_output": test["text_output"], "image_output": test["image_output"]}) + + if row["data_files"]: + exercise_dict["data_files"] = json.loads(row["data_files"]) + + if format_content: + exercise_dict["instructions"] = convert_markdown_to_html(convert_html_to_markdown(exercise_dict["instructions"])) # Removes html markup from instructions before converting markdown to html + exercise_dict["credit"] = convert_markdown_to_html(exercise_dict["credit"]) + exercise_dict["answer_description"] = convert_markdown_to_html(exercise_dict["answer_description"]) + exercise_dict["hint"] = convert_markdown_to_html(exercise_dict["hint"]) + + return exercise_dict + + def get_log_table_contents(self, file_path, year="No filter", month="No filter", day="No filter"): + new_dict = {} + line_num = 1 + with gzip.open(file_path) as read_file: + header = read_file.readline() + for line in read_file: + line_items = line.decode().rstrip("\n").split("\t") + + #Get ids to create links to each course, assignment, and exercise in the table + course_id = line_items[1] + assignment_id = line_items[2] + exercise_id = line_items[3] + + line_items[6] = f"{line_items[6]}" + line_items[7] = f"{line_items[7]}" + line_items[8] = f"{line_items[8]}" + + line_items = [line_items[0][:2], line_items[0][2:4], line_items[0][4:6], line_items[0][6:]] + line_items[4:] + + new_dict[line_num] = line_items + line_num += 1 + + # Filter by date. + year_dict = {} + month_dict = {} + day_dict = {} + + for key, line in new_dict.items(): + if year == "No filter" or line[0] == year: + year_dict[key] = line + for key, line in year_dict.items(): + if month == "No filter" or line[1] == month: + month_dict[key] = line + for key, line in month_dict.items(): + if day == "No filter" or line[2] == day: + day_dict[key] = line + + return day_dict + + def get_root_dirs_to_log(self): + root_dirs_to_log = set(["home", "course", "assignment", "exercise", "check_exercise", "edit_course", "edit_assignment", "edit_exercise", "delete_course", "delete_assignment", "delete_exercise", "view_answer", "import_course", "export_course"]) + return root_dirs_to_log + + def sort_nested_list(self, nested_list, key="title"): + l_dict = {} + for row in nested_list: + l_dict[row[1][key]] = row + + return [l_dict[key] for key in sort_nicely(l_dict)] + + def has_duplicate_title(self, entries, this_entry, proposed_title): + for entry in entries: + if entry[0] != this_entry and entry[1]["title"] == proposed_title: + return True + return False + + def save_course(self, course_basics, course_details): + if course_basics["exists"]: + sql = '''UPDATE courses + SET title = ?, visible = ?, introduction = ?, passcode = ?, date_updated = ?, consent_text = ?, consent_alternative_text = ? + WHERE course_id = ?''' + + self.execute(sql, [course_basics["title"], course_basics["visible"], course_details["introduction"], course_details["passcode"], course_details["date_updated"], course_details["consent_text"], course_details["consent_alternative_text"], course_basics["id"]]) + else: + sql = '''INSERT INTO courses (title, visible, introduction, passcode, date_created, date_updated, consent_text, consent_alternative_text) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)''' + + course_basics["id"] = self.execute(sql, [course_basics["title"], course_basics["visible"], course_details["introduction"], course_details["passcode"], course_details["date_created"], course_details["date_updated"], course_details["consent_text"], course_details["consent_alternative_text"]]) + course_basics["exists"] = True + + return course_basics["id"] + + def save_assignment(self, assignment_basics, assignment_details): + # Cleans and joins allowed_ip_addresses. + assignment_details["allowed_ip_addresses"] = ",".join(assignment_details["allowed_ip_addresses"]) + if assignment_details["allowed_ip_addresses"] == "": + assignment_details["allowed_ip_addresses"] = None + + if assignment_basics["exists"]: + sql = '''UPDATE assignments + SET title = ?, visible = ?, introduction = ?, date_updated = ?, start_date = ?, due_date = ?, allow_late = ?, late_percent = ?, view_answer_late = ?, enable_help_requests = ?, has_timer = ?, hour_timer = ?, minute_timer = ?, allowed_ip_addresses = ? + WHERE course_id = ? + AND assignment_id = ?''' + + self.execute(sql, [assignment_basics["title"], assignment_basics["visible"], assignment_details["introduction"], assignment_details["date_updated"], assignment_details["start_date"], assignment_details["due_date"], assignment_details["allow_late"], assignment_details["late_percent"], assignment_details["view_answer_late"], assignment_details["enable_help_requests"], assignment_details["has_timer"], assignment_details["hour_timer"], assignment_details["minute_timer"], assignment_details["allowed_ip_addresses"], assignment_basics["course"]["id"], assignment_basics["id"]]) + + else: + sql = '''INSERT INTO assignments (course_id, title, visible, introduction, date_created, date_updated, start_date, due_date, allow_late, late_percent, view_answer_late, enable_help_requests, has_timer, hour_timer, minute_timer, allowed_ip_addresses) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''' + + assignment_basics["id"] = self.execute(sql, [assignment_basics["course"]["id"], assignment_basics["title"], assignment_basics["visible"], assignment_details["introduction"], assignment_details["date_created"], assignment_details["date_updated"], assignment_details["start_date"], assignment_details["due_date"], assignment_details["allow_late"], assignment_details["late_percent"], assignment_details["view_answer_late"], assignment_details["enable_help_requests"], assignment_details["has_timer"], assignment_details["hour_timer"], assignment_details["minute_timer"], assignment_details["allowed_ip_addresses"]]) + + assignment_basics["exists"] = True + + # Returns allowed_ip_addresses to list. + if assignment_details["allowed_ip_addresses"]: + assignment_details["allowed_ip_addresses"] = assignment_details["allowed_ip_addresses"].split(",") + + return assignment_basics["id"] + + def save_exercise(self, exercise_basics, exercise_details): + # Only saves 'image_output' if it isn't blank. + if exercise_details["expected_image_output"] != "": + exercise_details["expected_image_output"] = "" if exercise_details["expected_image_output"].strip() == BLANK_IMAGE.strip() else exercise_details["expected_image_output"] + + if exercise_basics["exists"]: + sql = '''UPDATE exercises + SET title = ?, visible = ?, answer_code = ?, answer_description = ?, hint = ?, max_submissions = ?, + credit = ?, data_files = ?, back_end = ?, expected_text_output = ?, expected_image_output = ?, + instructions = ?, output_type = ?, show_answer = ?, show_student_submissions = ?, show_expected = ?, + show_test_code = ?, starter_code = ?, test_code = ?, date_updated = ?, enable_pair_programming = ?, check_code = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''' + + self.execute(sql, [exercise_basics["title"], exercise_basics["visible"], str(exercise_details["answer_code"]), exercise_details["answer_description"], exercise_details["hint"], exercise_details["max_submissions"], exercise_details["credit"], json.dumps(exercise_details["data_files"]), exercise_details["back_end"], exercise_details["expected_text_output"], exercise_details["expected_image_output"], exercise_details["instructions"], exercise_details["output_type"], exercise_details["show_answer"], exercise_details["show_student_submissions"], exercise_details["show_expected"], exercise_details["show_test_code"], exercise_details["starter_code"], exercise_details["test_code"], exercise_details["date_updated"], exercise_details["enable_pair_programming"], exercise_details["check_code"], exercise_basics["assignment"]["course"]["id"], exercise_basics["assignment"]["id"], exercise_basics["id"]]) + + sql = '''DELETE FROM tests + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''' + + self.execute(sql, [exercise_basics["assignment"]["course"]["id"], exercise_basics["assignment"]["id"], exercise_basics["id"]]) + + if len(exercise_details["tests"]) > 0: + for i in range(len(exercise_details["tests"])): + sql = '''INSERT INTO tests (course_id, assignment_id, exercise_id, code, test_instructions, text_output, image_output) + VALUES (?, ?, ?, ?, ?, ?, ?)''' + + test_id = self.execute(sql, [exercise_basics["assignment"]["course"]["id"], exercise_basics["assignment"]["id"], exercise_basics["id"], exercise_details["tests"][i]["code"], exercise_details["tests"][i]["test_instructions"], exercise_details["tests"][i]["text_output"], exercise_details["tests"][i]["image_output"]]) + + else: + sql = '''INSERT INTO exercises (course_id, assignment_id, title, visible, answer_code, answer_description, hint, max_submissions, credit, data_files, back_end, expected_text_output, expected_image_output, instructions, output_type, show_answer, show_student_submissions, show_expected, show_test_code, starter_code, test_code, date_created, date_updated, enable_pair_programming, check_code) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''' + + exercise_basics["id"] = self.execute(sql, [exercise_basics["assignment"]["course"]["id"], exercise_basics["assignment"]["id"], exercise_basics["title"], exercise_basics["visible"], str(exercise_details["answer_code"]), exercise_details["answer_description"], exercise_details["hint"], exercise_details["max_submissions"], exercise_details["credit"], json.dumps(exercise_details["data_files"]), exercise_details["back_end"], exercise_details["expected_text_output"], exercise_details["expected_image_output"], exercise_details["instructions"], exercise_details["output_type"], exercise_details["show_answer"], exercise_details["show_student_submissions"], exercise_details["show_expected"], exercise_details["show_test_code"], exercise_details["starter_code"], exercise_details["test_code"], exercise_details["date_created"], exercise_details["date_updated"], exercise_details["enable_pair_programming"], exercise_details["check_code"]]) + + exercise_basics["exists"] = True + + sql = '''DELETE FROM tests + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''' + + self.execute(sql, [exercise_basics["assignment"]["course"]["id"], exercise_basics["assignment"]["id"], exercise_basics["id"]]) + + if len(exercise_details["tests"]) > 0: + for i in range(len(exercise_details["tests"])): + sql = '''INSERT INTO tests (course_id, assignment_id, exercise_id, code, test_instructions, text_output, image_output) + VALUES (?, ?, ?, ?, ?, ?, ?)''' + + test_id = self.execute(sql, [exercise_basics["assignment"]["course"]["id"], exercise_basics["assignment"]["id"], exercise_basics["id"], exercise_details["tests"][i]["code"], exercise_details["tests"][i]["test_instructions"], exercise_details["tests"][i]["text_output"], exercise_details["tests"][i]["image_output"]]) + + return exercise_basics["id"] + + def save_presubmission(self, course, assignment, exercise, user, code): + sql = '''INSERT OR REPLACE INTO presubmissions (course_id, assignment_id, exercise_id, user_id, code) + VALUES (?, ?, ?, ?, ?)''' + + self.execute(sql, [int(course), int(assignment), int(exercise), user, code]) + + def save_submission(self, course, assignment, exercise, user, code, text_output, image_output, passed, tests, partner_id=None): + # Only saves 'image_output' if it isn't blank. + try: + if image_output != "": + image_output = "" if image_output.strip() == BLANK_IMAGE.strip() else image_output + + submission_id = self.get_next_submission_id(course, assignment, exercise, user) + sql = '''INSERT INTO submissions (course_id, assignment_id, exercise_id, user_id, submission_id, code, text_output, image_output, passed, date, partner_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''' + + self.execute(sql, [int(course), int(assignment), int(exercise), user, int(submission_id), code, text_output, image_output, passed, datetime.now(), partner_id]) + + if len(tests) > 0: + test_sql = '''INSERT OR REPLACE INTO submission_outputs (course_id, assignment_id, exercise_id, user_id, submission_id, text_output, image_output) + VALUES (?, ?, ?, ?, ?, ?, ?)''' + + for test in tests: + self.execute(test_sql, [int(course), int(assignment), int(exercise), user, int(submission_id), test["text_output"], test["image_output"]]) + + # Saves submission for partner. + if partner_id: + submission_id = self.get_next_submission_id(course, assignment, exercise, partner_id) + self.execute(sql, [int(course), int(assignment), int(exercise), partner_id, int(submission_id), code, text_output, image_output, passed, datetime.now(), user]) + + if len(tests) > 0: + for test in tests: + self.execute(test_sql, [int(course), int(assignment), int(exercise), partner_id, int(submission_id), test["text_output"], test["image_output"]]) + + except: + print(traceback.format_exc()) + + return submission_id + + def save_help_request(self, course, assignment, exercise, user_id, code, text_output, image_output, student_comment, date): + sql = '''INSERT INTO help_requests (course_id, assignment_id, exercise_id, user_id, code, text_output, image_output, student_comment, approved, date, more_info_needed) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''' + + self.execute(sql, (course, assignment, exercise, user_id, code, text_output, image_output, student_comment, 0, date, 0,)) + + def update_help_request(self, course, assignment, exercise, user_id, student_comment): + sql = '''UPDATE help_requests + SET student_comment = ?, more_info_needed = ?, suggestion = ?, approved = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ?''' + self.execute(sql, (student_comment, 0, None, 0, course, assignment, exercise, user_id,)) + + def delete_help_request(self, course, assignment, exercise, user_id): + sql = '''DELETE FROM help_requests + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ?''' + self.execute(sql, (course, assignment, exercise, user_id,)) + + def save_help_request_suggestion(self, course, assignment, exercise, user_id, suggestion, approved, suggester_id, approver_id, more_info_needed): + + sql = '''UPDATE help_requests + SET suggestion = ?, approved = ?, suggester_id = ?, approver_id = ?, more_info_needed = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ?''' + + self.execute(sql, (suggestion, approved, suggester_id, approver_id, more_info_needed, course, assignment, exercise, user_id,)) + + def copy_assignment(self, course_id, assignment_id, new_course_id): + sql = '''INSERT INTO assignments (course_id, title, visible, introduction, date_created, date_updated, start_date, due_date, allow_late, late_percent, view_answer_late, enable_help_requests, has_timer, hour_timer, minute_timer, allowed_ip_addresses) + SELECT ?, title, visible, introduction, date_created, date_updated, start_date, due_date, allow_late, late_percent, view_answer_late, enable_help_requests, has_timer, hour_timer, minute_timer, allowed_ip_addresses + FROM assignments + WHERE course_id = ? + AND assignment_id = ?''' + + new_assignment_id = self.execute(sql, (new_course_id, course_id, assignment_id,)) + + sql = '''INSERT INTO exercises (course_id, assignment_id, title, visible, answer_code, answer_description, hint, max_submissions, credit, data_files, back_end, expected_text_output, expected_image_output, instructions, output_type, show_answer, show_student_submissions, show_expected, show_test_code, starter_code, test_code, date_created, date_updated, enable_pair_programming) + SELECT ?, ?, title, visible, answer_code, answer_description, hint, max_submissions, credit, data_files, back_end, expected_text_output, expected_image_output, instructions, output_type, show_answer, show_student_submissions, show_expected, show_test_code, starter_code, test_code, date_created, date_updated, enable_pair_programming + FROM exercises + WHERE course_id = ? + AND assignment_id = ?''' + + self.execute(sql, (new_course_id, new_assignment_id, course_id, assignment_id,)) + + sql = '''INSERT INTO tests (course_id, assignment_id, exercise_id, code, test_instructions, text_output, image_output) + SELECT ?, ?, exercise_id, code, test_instructions, text_output, image_output + FROM tests + WHERE course_id = ? + AND assignment_id = ?''' + + self.execute(sql, (new_course_id, new_assignment_id, course_id, assignment_id,)) + + def update_user(self, user_id, user_dict): + self.set_user_dict_defaults(user_dict) + + sql = '''UPDATE users + SET name = ?, given_name = ?, family_name = ?, locale = ? + WHERE user_id = ?''' + + self.execute(sql, (user_dict["name"], user_dict["given_name"], user_dict["family_name"], user_dict["locale"], user_id,)) + + def update_user_settings(self, user_id, theme, use_auto_complete, enable_vim): + sql = '''UPDATE users + SET ace_theme = ?, use_auto_complete = ?, enable_vim = ? + WHERE user_id = ?''' + self.execute(sql, (theme, use_auto_complete, enable_vim, user_id)) + + def remove_user_submissions(self, user_id): + sql = '''SELECT submission_id + FROM submissions + WHERE user_id = ?''' + + submissions = self.fetchall(sql, (user_id,)) + if submissions: + + sql = '''DELETE FROM scores + WHERE user_id = ?''' + self.execute(sql, (user_id,)) + + sql = '''DELETE FROM submissions + WHERE user_id = ?''' + self.execute(sql, (user_id,)) + + return True + else: + return False + + def delete_user(self, user_id): + sql = '''DELETE FROM users + WHERE user_id = ?''' + + self.execute(sql, (user_id,)) + + def move_exercise(self, course_id, assignment_id, exercise_id, new_assignment_id): + self.execute('''UPDATE exercises + SET assignment_id = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (new_assignment_id, course_id, assignment_id, exercise_id, )) + + self.execute('''UPDATE scores + SET assignment_id = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (new_assignment_id, course_id, assignment_id, exercise_id, )) + + self.execute('''UPDATE submissions + SET assignment_id = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (new_assignment_id, course_id, assignment_id, exercise_id, )) + + self.execute('''UPDATE presubmissions + SET assignment_id = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (new_assignment_id, course_id, assignment_id, exercise_id, )) + + self.execute('''UPDATE submission_outputs + SET assignment_id = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (new_assignment_id, course_id, assignment_id, exercise_id, )) + + self.execute('''UPDATE tests + SET assignment_id = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (new_assignment_id, course_id, assignment_id, exercise_id, )) + + def copy_exercise(self, course_id, assignment_id, exercise_id, new_title): + try: + sql = '''INSERT INTO exercises (course_id, assignment_id, title, visible, answer_code, answer_description, hint, max_submissions, credit, data_files, back_end, expected_text_output, expected_image_output, instructions, output_type, show_answer, show_student_submissions, show_expected, show_test_code, starter_code, test_code, date_created, date_updated, enable_pair_programming, check_code) + SELECT course_id, assignment_id, ?, visible, answer_code, answer_description, hint, max_submissions, credit, data_files, back_end, expected_text_output, expected_image_output, instructions, output_type, show_answer, show_student_submissions, show_expected, show_test_code, starter_code, test_code, date_created, date_updated, enable_pair_programming, check_code + FROM exercises + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''' + + new_exercise_id = self.execute(sql, (new_title, course_id, assignment_id, exercise_id, )) + + sql = '''INSERT INTO tests (course_id, assignment_id, exercise_id, code, test_instructions, text_output, image_output) + SELECT course_id, assignment_id, ?, code, test_instructions, text_output, image_output + FROM tests + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''' + + self.execute(sql, (new_exercise_id, course_id, assignment_id, exercise_id, )) + except: + print(traceback.format_exc()) + + def delete_exercise(self, exercise_basics): + course_id = exercise_basics["assignment"]["course"]["id"] + assignment_id = exercise_basics["assignment"]["id"] + exercise_id = exercise_basics["id"] + + self.execute('''DELETE FROM submissions + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + self.execute('''DELETE FROM presubmissions + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + self.execute('''DELETE FROM scores + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + self.execute('''DELETE FROM exercises + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + self.execute('''DELETE FROM tests + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + self.execute('''DELETE FROM submission_outputs + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + def delete_assignment(self, assignment_basics): + course_id = assignment_basics["course"]["id"] + assignment_id = assignment_basics["id"] + + self.execute('''DELETE FROM tests + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + self.execute('''DELETE FROM submission_outputs + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + self.execute('''DELETE FROM presubmissions + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + self.execute('''DELETE FROM submissions + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + self.execute('''DELETE FROM scores + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + self.execute('''DELETE FROM exercises + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + self.execute('''DELETE FROM assignments + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + def delete_course(self, course_basics): + course_id = course_basics["id"] + + self.execute('''DELETE FROM presubmissions + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM tests + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM submission_outputs + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM submissions + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM exercises + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM assignments + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM courses + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM permissions + WHERE course_id = ?''', (course_id, )) + + def delete_course_submissions(self, course_basics): + course_id = course_basics["id"] + + self.execute('''DELETE FROM submissions + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM scores + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM presubmissions + WHERE course_id = ?''', (course_id, )) + + self.execute('''DELETE FROM submission_outputs + WHERE course_id = ?''', (course_id, )) + + def delete_assignment_submissions(self, assignment_basics): + course_id = assignment_basics["course"]["id"] + assignment_id = assignment_basics["id"] + + self.execute('''DELETE FROM submissions + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + self.execute('''DELETE FROM scores + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + self.execute('''DELETE FROM presubmissions + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + self.execute('''DELETE FROM submission_outputs + WHERE course_id = ? + AND assignment_id = ?''', (course_id, assignment_id, )) + + def delete_exercise_submissions(self, exercise_basics): + course_id = exercise_basics["assignment"]["course"]["id"] + assignment_id = exercise_basics["assignment"]["id"] + exercise_id = exercise_basics["id"] + + self.execute('''DELETE FROM submissions + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + self.execute('''DELETE FROM scores + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + self.execute('''DELETE FROM presubmissions + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + self.execute('''DELETE FROM submission_outputs + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ?''', (course_id, assignment_id, exercise_id, )) + + def create_scores_text(self, course_id, assignment_id): + out_file_text = "Course_ID,Assignment_ID,Student_ID,Score\n" + scores = self.get_assignment_scores(course_id, assignment_id) + + for student in scores: + out_file_text += f"{course_id},{assignment_id},{student[0]},{student[1]['percent_passed']}\n" + + return out_file_text + + def export_data(self, course_basics, table_name, output_tsv_file_path): + if table_name == "submissions": + sql = '''SELECT c.title, a.title, e.title, s.user_id, s.submission_id, s.code, s.text_output, s.image_output, s.passed, s.date + FROM submissions s + INNER JOIN courses c + ON c.course_id = s.course_id + INNER JOIN assignments a + ON a.assignment_id = s.assignment_id + INNER JOIN exercises e + ON e.exercise_id = s.exercise_id + WHERE s.course_id = ?''' + + else: + sql = f"SELECT * FROM {table_name} WHERE course_id = ?" + + rows = [] + for row in self.fetchall(sql, (course_basics["id"],)): + row_values = [] + for x in row: + if type(x) is datetime: + x = str(x) + row_values.append(x) + + rows.append(row_values) + + with open(output_tsv_file_path, "w") as out_file: + out_file.write(json.dumps(rows)) + + def create_zip_file_path(self, descriptor): + temp_dir_path = "/database/tmp/{}".format(create_id()) + zip_file_name = f"{descriptor}.zip" + zip_file_path = f"{temp_dir_path}/{zip_file_name}" + return temp_dir_path, zip_file_name, zip_file_path + + def zip_export_files(self, temp_dir_path, zip_file_name, zip_file_path, descriptor): + os.system(f"cp VERSION {temp_dir_path}/{descriptor}/") + os.system(f"cd {temp_dir_path}; zip -r -qq {zip_file_path} .") + + def create_export_paths(self, temp_dir_path, descriptor): + os.makedirs(temp_dir_path) + os.makedirs(f"{temp_dir_path}/{descriptor}") + + def remove_export_paths(self, zip_file_path, tmp_dir_path): + if os.path.exists(zip_file_path): + os.remove(zip_file_path) + + if os.path.exists(tmp_dir_path): + shutil.rmtree(tmp_dir_path, ignore_errors=True) + + def rebuild_exercises(self, assignment_title=None): + with open("/logs/progress.log", "w") as progress_file: + progress_file.write("Retrieving exercises that need to be rebuilt...\n") + progress_file.flush() + + if assignment_title: + sql = '''SELECT e.* + FROM exercises e + INNER JOIN assignments a + ON e.course_id = a.course_id AND e.assignment_id = a.assignment_id + WHERE a.title = ?''' + exercises = self.fetchall(sql, (assignment_title, )) + else: + sql = '''SELECT e.* + FROM exercises e''' + exercises = self.fetchall(sql) + + for row in exercises: + course = row["course_id"] + assignment = row["assignment_id"] + exercise = row["exercise_id"] + + progress_file.write(f"Rebuilding course {course}, assignment {assignment}, exercise {exercise}.\n") + progress_file.flush() + + exercise_basics = self.get_exercise_basics(course, assignment, exercise) + exercise_details = self.get_exercise_details(course, assignment, exercise) + + text_output, image_output, tests = exec_code(self.__settings_dict, exercise_details["answer_code"], exercise_basics, exercise_details) + + exercise_details["tests"] = [{k: v for k, v in d.items() if k != 'text_output' and k != "image_output"} for d in exercise_details["tests"]] + + tests_dict = [] + for i in range(len(tests)): + tests_dict.append({**tests[i], **exercise_details["tests"][i]}) + + exercise_details["tests"] = tests_dict + exercise_details["expected_text_output"] = text_output.strip() + exercise_details["expected_image_output"] = image_output + + self.save_exercise(exercise_basics, exercise_details) + + progress_file.write(f"Done rebuilding exercises.\n") + progress_file.flush() + + def rerun_submissions(self, assignment_title=None): + with open("/logs/progress.log", "a") as progress_file: + progress_file.write("Retrieving submissions that need to be rerun...\n") + + if assignment_title: + sql = '''SELECT course_id, assignment_id + FROM assignments + WHERE title = ?''' + + row = self.fetchone(sql, (assignment_title, )) + course = int(row["course_id"]) + assignment = int(row["assignment_id"]) + + sql = '''SELECT course_id, assignment_id, exercise_id, user_id, submission_id, code + FROM submissions + WHERE course_id = ? AND assignment_id = ? AND passed = 0 + ORDER BY exercise_id, user_id, submission_id''' + submissions = self.fetchall(sql, (course, assignment, )) + else: + sql = '''SELECT course_id, assignment_id, exercise_id, user_id, submission_id, code + FROM submissions + ORDER BY exercise_id, user_id, submission_id LIMIT 1000''' + submissions = self.fetchall(sql) + + for row in submissions: + course = row["course_id"] + assignment = row["assignment_id"] + exercise = row["exercise_id"] + user = row["user_id"] + submission = row["submission_id"] + code = row["code"].replace("\r", "") + + progress_file.write(f"Rerunning submission {submission} for course {course}, assignment {assignment}, exercise {exercise}, user {user}.\n") + progress_file.flush() + + exercise_basics = self.get_exercise_basics(course, assignment, exercise) + exercise_details = self.get_exercise_details(course, assignment, exercise) + + text_output, image_output, tests = exec_code(self.__settings_dict, code, exercise_basics, exercise_details, None) + diff, passed, tests = check_exercise_output(exercise_details, text_output, image_output, tests) + + sql = '''UPDATE submissions + SET text_output = ?, + image_output = ?, + passed = ? + WHERE course_id = ? + AND assignment_id = ? + AND exercise_id = ? + AND user_id = ? + AND submission_id = ?''' + + self.execute(sql, [text_output, image_output, passed, int(course), int(assignment), int(exercise), user, int(submission)]) + + for test in tests: + sql = '''INSERT INTO submission_outputs (course_id, assignment_id, exercise_id, user_id, submission_id, text_output, image_output) + VALUES (?, ?, ?, ?, ?, ?, ?)''' + + self.execute(sql, [int(course), int(assignment), int(exercise), user, int(submission), test["text_output"], test["image_output"], ]) + + progress_file.write(f"Done rerunning submissions.\n") + progress_file.flush() diff --git a/front_end/handlers/BaseUserHandler.py b/front_end/handlers/BaseUserHandler.py index 653071e3..a750d10c 100644 --- a/front_end/handlers/BaseUserHandler.py +++ b/front_end/handlers/BaseUserHandler.py @@ -6,7 +6,7 @@ class BaseUserHandler(RequestHandler): def prepare(self): self.settings_dict = load_yaml_dict(read_file("/Settings.yaml")) - self.content = Content(self.settings_dict) + self.content = ContentSQLite(self.settings_dict) self.user_info_var = contextvars.ContextVar("user_info") self.user_is_administrator_var = contextvars.ContextVar("user_is_administrator") self.user_instructor_courses_var = contextvars.ContextVar("user_instructor_courses") diff --git a/front_end/handlers/CASLoginHandler.py b/front_end/handlers/CASLoginHandler.py index 3c462a24..622e8629 100644 --- a/front_end/handlers/CASLoginHandler.py +++ b/front_end/handlers/CASLoginHandler.py @@ -8,7 +8,7 @@ class CASLoginHandler(RequestHandler): def prepare(self): self.settings_dict = load_yaml_dict(read_file("/Settings.yaml")) - self.content = Content(self.settings_dict) + self.content = ContentSQLite(self.settings_dict) async def get(self): try: diff --git a/front_end/handlers/DevelopmentLoginHandler.py b/front_end/handlers/DevelopmentLoginHandler.py index 4b8a5374..a7dfb1eb 100644 --- a/front_end/handlers/DevelopmentLoginHandler.py +++ b/front_end/handlers/DevelopmentLoginHandler.py @@ -4,7 +4,7 @@ class DevelopmentLoginHandler(RequestHandler): def prepare(self): self.settings_dict = load_yaml_dict(read_file("/Settings.yaml")) - self.content = Content(self.settings_dict) + self.content = ContentSQLite(self.settings_dict) def get(self): self.render("devlogin.html", courses=self.content.get_courses(False)) diff --git a/front_end/handlers/GoogleLoginHandler.py b/front_end/handlers/GoogleLoginHandler.py index 24e455f5..0b7f7b4d 100644 --- a/front_end/handlers/GoogleLoginHandler.py +++ b/front_end/handlers/GoogleLoginHandler.py @@ -5,7 +5,7 @@ class GoogleLoginHandler(RequestHandler, GoogleOAuth2Mixin): def prepare(self): self.settings_dict = load_yaml_dict(read_file("/Settings.yaml")) - self.content = Content(self.settings_dict) + self.content = ContentSQLite(self.settings_dict) async def get(self): try: diff --git a/front_end/handlers/HomeHandler.py b/front_end/handlers/HomeHandler.py index 77f6f5b8..921ba44a 100644 --- a/front_end/handlers/HomeHandler.py +++ b/front_end/handlers/HomeHandler.py @@ -8,7 +8,7 @@ def prepare(self): self.user_info_var = contextvars.ContextVar("user_info") self.user_is_administrator_var = contextvars.ContextVar("user_is_administrator") self.settings_dict = load_yaml_dict(read_file("/Settings.yaml")) - self.content = Content(self.settings_dict) + self.content = ContentSQLite(self.settings_dict) try: user_id = self.get_secure_cookie("user_id") diff --git a/front_end/html/exercise.html b/front_end/html/exercise.html index c505b4cf..b8ebe64e 100644 --- a/front_end/html/exercise.html +++ b/front_end/html/exercise.html @@ -307,27 +307,26 @@
Using Pair Programming:
+
+

Assignment: {{ exercise_basics["assignment"]["title"] }}

+
+
+
Exercise: {{ exercise_basics["title"] }}
+
-
-
-

Assignment: {{ exercise_basics["assignment"]["title"] }}

-
-
-
Exercise: {{ exercise_basics["title"] }}
-
- -
- {% if is_administrator or is_instructor or is_assistant %} - Edit exercise - {% end %} - {% if prev_exercise %} - Previous exercise - {% end %} - {% if next_exercise %} - Next exercise - {% end %} +
+ {% if is_administrator or is_instructor or is_assistant %} + Edit exercise + {% end %} + {% if prev_exercise %} + Previous exercise + {% end %} + {% if next_exercise %} + Next exercise + {% end %} +
+
-
diff --git a/front_end/migration_scripts/15_to_16.py b/front_end/migration_scripts/15_to_16.py index 73704c91..312fa424 100644 --- a/front_end/migration_scripts/15_to_16.py +++ b/front_end/migration_scripts/15_to_16.py @@ -8,7 +8,7 @@ from content import * settings_dict = load_yaml_dict(read_file("/Settings.yaml")) -content = Content(settings_dict) +content = ContentSQLite(settings_dict) version = read_file("/VERSION").rstrip() diff --git a/front_end/migration_scripts/15_to_16.sql b/front_end/migration_scripts/15_to_16.sql index 17385448..4db1e05a 100644 --- a/front_end/migration_scripts/15_to_16.sql +++ b/front_end/migration_scripts/15_to_16.sql @@ -39,7 +39,7 @@ INSERT INTO tests (course_id, assignment_id, exercise_id, code, test_instruction AND test_code != "" AND show_test_code = 0; -DROP TABLE course_registration; +DROP TABLE IF EXISTS course_registration; DELETE FROM course_registrations WHERE user_id NOT IN (SELECT user_id FROM permissions WHERE role = 'administrator' or role = 'instructor'); @@ -47,11 +47,11 @@ DELETE FROM submissions WHERE user_id NOT IN (SELECT user_id FROM permissions WH DELETE FROM metadata WHERE version = 5; -DROP TABLE problems; +DROP TABLE IF EXISTS problems; DELETE FROM scores WHERE user_id NOT IN (SELECT user_id FROM permissions WHERE role = 'administrator' or role = 'instructor'); -DROP TABLE user_assignment_start; +DROP TABLE IF EXISTS user_assignment_start; DELETE FROM user_assignment_starts WHERE user_id NOT IN (SELECT user_id FROM permissions WHERE role = 'administrator' or role = 'instructor'); diff --git a/front_end/migration_scripts/19_to_20_migrate.sql b/front_end/migration_scripts/19_to_20_migrate.sql index ced6b911..1c915564 100644 --- a/front_end/migration_scripts/19_to_20_migrate.sql +++ b/front_end/migration_scripts/19_to_20_migrate.sql @@ -12,8 +12,8 @@ DELETE FROM users; -- Delete old tables -DROP TABLE course_registration; -DROP TABLE user_assignment_start; +DROP TABLE IF EXISTS course_registration; +DROP TABLE IF EXISTS user_assignment_start; -- Add email_address field to users table. diff --git a/front_end/migration_scripts/20_to_21.py b/front_end/migration_scripts/20_to_21.py new file mode 100644 index 00000000..dfba1085 --- /dev/null +++ b/front_end/migration_scripts/20_to_21.py @@ -0,0 +1,123 @@ +import atexit +import sqlite3 +import sys +import traceback + +sys.path.append('/app') +from helper import * +from content import * +from content_maria import * + +settings_dict = load_yaml_dict(read_file("/Settings.yaml")) +content = ContentSQLite(settings_dict) + +version = read_file("/VERSION").rstrip() + +# This tells us whether the migration has already happened. +check_sql = '''SELECT COUNT(*) AS count + FROM pragma_table_info("tests") + WHERE name = "go"''' + +if content.fetchone(check_sql)["count"] > 0: + print("***NotNeeded***") +else: + with open("/migration_scripts/20_to_21.sql") as sql_file: + sql_statements = sql_file.read().split(";") + + try: + with open("/logs/progress.log", "w") as progress_file: + # Execute migration script's sql commands + for sql in sql_statements: + progress_file.write(sql + "\n") + content.execute(sql) + + progress_file.write('commence migration \n----------------------------------------------------------\n\n') + + # Dumps the current SQLite database into a sql file that is converted into MYSQL. + sqlite_dump_name = content.dump_database() + + # Switch to content_mariadb. + content = Content(settings_dict) + + with open(sqlite_dump_name) as db_dump: + # These tables shouldn't be present in the current version, but if they slipped through, do not migrate them over to MariaDB. + old_tables = ['course_registration', 'user_assignment_start', 'problems'] + + create_statements = [] + other_statements = [] + + missing_parantheses = None + + # Splits sql commands on semicolons that aren't inside strings + for sql_command in re.split(r"(?!\B[`'][^`']*);(?![^`']*[`']\B)", db_dump.read()): + sql_command = sql_command.strip() + + # Joins sql fragments together if needed. + if missing_parantheses is not None: + sql_command = f"{missing_parantheses};{sql_command}" + + # If sql command is missing a closing paranthesis, flag it as a fragment and start scanning for the end. + if not sql_command.endswith(")"): + missing_parantheses = sql_command + else: + missing_parantheses = None + if "TABLE" in sql_command: + # Adds all create statements to a list + if 'CREATE TABLE' in sql_command: + # Excludes any table found in 'old_tables' + if all([f'`{table}`' not in sql_command for table in old_tables]): + create_statements.append(sql_command) + else: + # Adds all other statements to a separate list + other_statements.append(sql_command) + + # Specifies order that create statements should be run in + order = ['courses','users','assignments','exercises','submissions','tests','presubmissions','submission_outputs','help_requests','metadata','scores','course_registrations','permissions','user_assignment_starts'] + order = [f"CREATE TABLE IF NOT EXISTS `{o}`" for o in order] + ordered_create_statements = [] + + # Provides error logging if the order list and create statements list are out of sync (I can't picture that this would happen anymore, but it's an important bug to check for) + if len(order) != len(create_statements): + create_statements = [x.split(" (")[0] for x in create_statements] + + greater = order if len(order) > len(create_statements) else create_statements + lesser = order if len(order) < len(create_statements) else create_statements + missing = list(set(greater) - set(lesser)) + + # Writes the missing statements in progress.log + for m in missing: + progress_file.write(f"missing: {m}" + "\n") + + sys.exit(1) + + # Surely there's a cleaner, more efficient way to order the create statements but this is what I used + for i in range(len(order)): + for j in range(len(create_statements)): + if create_statements[j].strip().startswith(order[i]): + ordered_create_statements.append(create_statements[j]) + break + + # Temporarily allows adding of rows before the addition of rows they might reference as foreign keys + content.execute("SET FOREIGN_KEY_CHECKS=0") + + for c in ordered_create_statements: + # Executes each create statement in order + progress_file.write(c + "\n") + content.execute(c) + + for o in other_statements: + # Executes all other statements + progress_file.write(o + "\n") + content.execute(o) + + # Updates metadata values and returns foreign_key_checks to on + content.execute("INSERT INTO `metadata` VALUES(21);") + content.execute("SET FOREIGN_KEY_CHECKS=1") + + print("***Success***") + + except: + print(traceback.format_exc()) + + with open("/logs/progress.log", "a") as progress_file: + progress_file.write(traceback.format_exc()) diff --git a/front_end/migration_scripts/20_to_21.sql b/front_end/migration_scripts/20_to_21.sql new file mode 100644 index 00000000..f16f5a22 --- /dev/null +++ b/front_end/migration_scripts/20_to_21.sql @@ -0,0 +1,39 @@ +-- I included this line because in some of the migration runs I was seeing errors that 'show_test_code' (an obselete column, would be useful to track down each instance and remove it from the code entirely given the time) +-- was having non integers assigned to it. I didn't have the time to fully track down those errors or why an empty string would be assigned to show_test_code in the first place, so I'm leaving this here as is. + +-- All this is doing is changing the type of show_test_code to text +CREATE TABLE IF NOT EXISTS exercises2 ( + course_id integer NOT NULL, + assignment_id integer NOT NULL, + exercise_id integer PRIMARY KEY AUTOINCREMENT, + title text NOT NULL, + visible integer NOT NULL, + answer_code text NOT NULL, + answer_description text, + hint text, + max_submissions integer NOT NULL, + credit text, + data_files text, + back_end text NOT NULL, + expected_text_output text NOT NULL, + expected_image_output text NOT NULL, + instructions text NOT NULL, + output_type text NOT NULL, + show_answer integer NOT NULL, + show_student_submissions integer NOT NULL, + show_expected integer NOT NULL, + show_test_code text, + starter_code text, + test_code text, + date_created timestamp NOT NULL, + date_updated timestamp NOT NULL, enable_pair_programming integer NOT NULL DEFAULT 0, check_code text DEFAULT "", + FOREIGN KEY (course_id) REFERENCES courses (course_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (assignment_id) REFERENCES assignments (assignment_id) ON DELETE CASCADE ON UPDATE CASCADE); + +INSERT INTO exercises2 + SELECT * + FROM exercises; + +DROP TABLE exercises; + +ALTER TABLE exercises2 RENAME TO exercises; diff --git a/front_end/migration_scripts/migrate.py b/front_end/migration_scripts/migrate.py index 33ab8b27..7c998c7c 100644 --- a/front_end/migration_scripts/migrate.py +++ b/front_end/migration_scripts/migrate.py @@ -20,7 +20,7 @@ migrate_file_path = f"/migration_scripts/{migration_numbers}_migrate.sql" settings_dict = load_yaml_dict(read_file("/Settings.yaml")) -content = Content(settings_dict) +content = ContentSQLite(settings_dict) check_sql = read_file(check_file_path) diff --git a/front_end/scheduled_scripts/summarize_logs.py b/front_end/scheduled_scripts/summarize_logs.py index 37d26d5f..d2706d71 100644 --- a/front_end/scheduled_scripts/summarize_logs.py +++ b/front_end/scheduled_scripts/summarize_logs.py @@ -8,7 +8,7 @@ from content import * settings_dict = load_yaml_dict(read_file("/Settings.yaml")) -content = Content(settings_dict) +content = ContentSQLite(settings_dict) #print("Debugging:") #print(sys.argv) diff --git a/front_end/webserver.py b/front_end/webserver.py index b7395b9a..9217acad 100644 --- a/front_end/webserver.py +++ b/front_end/webserver.py @@ -140,7 +140,8 @@ async def get(self, file_name): "secret": secrets_dict["google_oauth_secret"]} settings_dict = load_yaml_dict(read_file("/Settings.yaml")) - content = Content(settings_dict) + content = ContentSQLite(settings_dict) + content.create_database_tables() database_version = content.get_database_version() code_version = int(read_file("VERSION").rstrip()) @@ -162,7 +163,8 @@ async def get(self, file_name): print("Database migration not needed.") elif "***Success***" in result: print(f"Database successfully migrated to version {v+1}.") - content.update_database_version(code_version) + new_version = v + 1 + content.update_database_version(new_version) else: print(f"Database migration failed for verson {v+1}, so rolling back...") print(result)