├── usage ├── __init__.py ├── migrations │ └── __init__.py ├── tests.py ├── admin.py ├── models.py ├── apps.py ├── urls.py ├── templates │ └── usage.html └── views.py ├── feedback ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_alter_feedback_options.py │ └── 0001_initial.py ├── admin.py ├── tests.py ├── apps.py ├── forms.py ├── urls.py ├── models.py ├── views.py └── templates │ └── feedback │ └── feedback.html ├── management ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0004_auto_20200302_1349.py │ ├── 0003_mysql_cache.py │ ├── 0002_auto_20190108_0943.py │ └── 0001_initial.py ├── tests.py ├── admin.py ├── apps.py ├── templates │ └── management │ │ ├── index.html │ │ ├── user_add.html │ │ ├── cohort_add.html │ │ ├── base.html │ │ ├── cohort_members.html │ │ ├── pagination.html │ │ ├── user_list.html │ │ └── cohort_list.html ├── urls.py ├── import_fixtures.py ├── models.py ├── forms.py ├── fixtures │ ├── student_mentor_mappings.json │ └── cohorts.json └── views.py ├── seumich ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0003_auto_20160505_1353.py │ └── 0002_custom_add_composite_primary_keys.py ├── templatetags │ ├── __init__.py │ └── filters.py ├── templates │ ├── seumich │ │ ├── advisor.html │ │ ├── student.html │ │ ├── student_info_partial.html │ │ ├── cohort_detail.html │ │ ├── class_site_detail.html │ │ ├── student_list.html │ │ ├── advisor_detail.html │ │ ├── cohort_list.html │ │ ├── ts_pager.html │ │ ├── advisor_without_students.html │ │ ├── advisor_list.html │ │ ├── student_list_partial.html │ │ └── student_detail.html │ └── base.html ├── static │ └── seumich │ │ ├── images │ │ ├── icon.png │ │ ├── favicon.ico │ │ ├── yeoman.png │ │ ├── Dropdown_Plus.png │ │ ├── Dropdown_Minus.png │ │ ├── Status_Icons_Green.png │ │ ├── Status_Icons_Red.png │ │ ├── Status_Icons_Student.png │ │ ├── Status_Icons_Yellow.png │ │ ├── Status_Icons_ClassAverage.png │ │ ├── Status_Icons_StudentAlert.png │ │ ├── Status_Icons_Not Applicable.png │ │ ├── se-logo.svg │ │ └── feedback.svg │ │ ├── student.js │ │ ├── styles │ │ ├── _secolor.scss │ │ ├── _semixins.scss │ │ └── _sefont.scss │ │ ├── index.js │ │ ├── sort_table.js │ │ └── assignment_list_table.js ├── fixtures │ └── 0_init.sql ├── routers.py ├── mixins.py ├── urls.py └── views.py ├── student_explorer ├── __init__.py ├── common │ ├── __init__.py │ └── db_util.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── import_manage_fixtures.py │ │ └── createuser.py ├── templates │ ├── robots.txt │ ├── 400.html │ ├── 500.html │ ├── 404.html │ ├── 403.html │ ├── 405.html │ └── student_explorer │ │ ├── about.html │ │ └── index.html ├── local │ └── README.md ├── context_processors.py ├── wsgi.py ├── views.py ├── middleware.py ├── backends.py ├── urls.py └── fixtures │ ├── dev_users_profiles.json │ └── dev_users.json ├── tracking ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── urls.py ├── eventnames.py ├── views.py ├── __init__.py ├── models.py └── utils.py ├── mysql └── init.sql ├── style_guide ├── se-color-palette.png └── se-style-guide.png ├── NOTICE ├── .vscode ├── settings.json └── launch.json ├── manage.py ├── package.json ├── start-localhost.sh ├── requirements.txt ├── start.sh ├── docker-compose.yml ├── .gitignore ├── Dockerfile ├── .env.sample ├── README.md └── LICENSE /usage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feedback/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /seumich/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /student_explorer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /usage/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /feedback/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /management/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /seumich/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /seumich/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tracking/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /student_explorer/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /student_explorer/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /student_explorer/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /student_explorer/templates/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /usage/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /feedback/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /feedback/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /management/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /usage/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /management/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /usage/models.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from django.db import models 4 | 5 | # Create your models here. 6 | -------------------------------------------------------------------------------- /usage/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsageConfig(AppConfig): 5 | name = 'usage' 6 | -------------------------------------------------------------------------------- /mysql/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS student_explorer; 2 | GRANT ALL PRIVILEGES ON *.* TO student_explorer; 3 | -------------------------------------------------------------------------------- /feedback/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FeedbackConfig(AppConfig): 5 | name = 'feedback' 6 | -------------------------------------------------------------------------------- /management/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ManagementConfig(AppConfig): 5 | name = 'management' 6 | -------------------------------------------------------------------------------- /seumich/templates/seumich/advisor.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block content %}{% endblock %} 4 | -------------------------------------------------------------------------------- /seumich/templates/seumich/student.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block content %}{% endblock %} 4 | -------------------------------------------------------------------------------- /style_guide/se-color-palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/style_guide/se-color-palette.png -------------------------------------------------------------------------------- /style_guide/se-style-guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/style_guide/se-style-guide.png -------------------------------------------------------------------------------- /seumich/static/seumich/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/icon.png -------------------------------------------------------------------------------- /seumich/static/seumich/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/favicon.ico -------------------------------------------------------------------------------- /seumich/static/seumich/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/yeoman.png -------------------------------------------------------------------------------- /feedback/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class FeedbackForm(forms.Form): 5 | feedback_message = forms.CharField(widget=forms.Textarea) 6 | -------------------------------------------------------------------------------- /seumich/static/seumich/images/Dropdown_Plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/Dropdown_Plus.png -------------------------------------------------------------------------------- /seumich/static/seumich/images/Dropdown_Minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/Dropdown_Minus.png -------------------------------------------------------------------------------- /seumich/fixtures/0_init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS student_explorer; 2 | GRANT ALL PRIVILEGES ON *.* TO 'student_explorer'@'%' IDENTIFIED BY 'student_explorer'; 3 | -------------------------------------------------------------------------------- /seumich/static/seumich/images/Status_Icons_Green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/Status_Icons_Green.png -------------------------------------------------------------------------------- /seumich/static/seumich/images/Status_Icons_Red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/Status_Icons_Red.png -------------------------------------------------------------------------------- /seumich/static/seumich/images/Status_Icons_Student.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/Status_Icons_Student.png -------------------------------------------------------------------------------- /seumich/static/seumich/images/Status_Icons_Yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/Status_Icons_Yellow.png -------------------------------------------------------------------------------- /tracking/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from tracking import views 3 | 4 | urlpatterns = [ 5 | path('record-event/', views.record_event, name="record-event"), 6 | ] -------------------------------------------------------------------------------- /seumich/static/seumich/images/Status_Icons_ClassAverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/Status_Icons_ClassAverage.png -------------------------------------------------------------------------------- /seumich/static/seumich/images/Status_Icons_StudentAlert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/Status_Icons_StudentAlert.png -------------------------------------------------------------------------------- /seumich/static/seumich/images/Status_Icons_Not Applicable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tl-its-umich-edu/student_explorer/HEAD/seumich/static/seumich/images/Status_Icons_Not Applicable.png -------------------------------------------------------------------------------- /feedback/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from feedback import views 3 | 4 | app_name = 'feedback' 5 | 6 | urlpatterns = [ 7 | path('', views.submitFeedback, name='feedback'), 8 | ] 9 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Student Explorer 2 | Copyright © 2015 The Regents of the University of Michigan 3 | 4 | This product includes software developed at 5 | The Digital Innovation Greenhouse (DIG), Academic Innovation, University of Michigan (http://ai.umich.edu/). 6 | -------------------------------------------------------------------------------- /seumich/static/seumich/student.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | var currentClassCode; 4 | 5 | currentClassCode = $('#current-classsite-code').val(); 6 | $('div[id^=student-menu-' + currentClassCode + ']').css('background-color', '#CCCCCC'); 7 | 8 | }); 9 | -------------------------------------------------------------------------------- /usage/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from usage import views 3 | 4 | app_name = 'usage' 5 | 6 | urlpatterns = [ 7 | path('', views.UsageView.as_view(), name='usage_index'), 8 | path('download/', views.DownloadCsvView.as_view(), name='usage_download'), 9 | ] 10 | -------------------------------------------------------------------------------- /student_explorer/local/README.md: -------------------------------------------------------------------------------- 1 | # Student Explorer Local Settings # 2 | 3 | The contents of this directory are not versioned (except for this file). Its purpose is to provide a location to store development settings and supporting files like certificates and keys that should be kept private. 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "autopep8", 3 | "python.formatting.autopep8Args": ["--ignore=E501", "--max-line-length=120"], 4 | "python.linting.pycodestyleArgs": ["--ignore=E501", "--max-line-length=120"], 5 | "python.linting.pylintArgs": ["--load-plugins=pylint_django"] 6 | } 7 | -------------------------------------------------------------------------------- /student_explorer/context_processors.py: -------------------------------------------------------------------------------- 1 | from student_explorer.common import db_util 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | def last_updated(request): 7 | results = db_util.get_data_date() 8 | return {'data_time': results.get("data_time"), 9 | 'data_schema': results.get("data_schema")} -------------------------------------------------------------------------------- /management/templates/management/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'management/base.html' %} 2 | 3 | {% block management %} 4 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /student_explorer/templates/400.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block title %} 4 | Bad Request 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

Bad Request

10 |

Sorry, your browser sent a request that this server could not understand.

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /feedback/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | from django_extensions.db.models import TimeStampedModel 4 | 5 | 6 | class Feedback(TimeStampedModel): 7 | user = models.ForeignKey(User, on_delete=models.CASCADE) 8 | feedback_message = models.TextField() 9 | 10 | def __str__(self): 11 | return self.feedback_message 12 | -------------------------------------------------------------------------------- /student_explorer/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block title %} 4 | Internal Server Error 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

Server Error

10 |

Sorry, the server encountered an internal error and was unable to complete your request.

11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /feedback/migrations/0002_alter_feedback_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-02-10 19:18 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('feedback', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='feedback', 15 | options={'get_latest_by': 'modified'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", 7 | "student_explorer.local.settings_override") 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | if len(sys.argv) == 2 and sys.argv[1] == "runserver": 12 | sys.argv.append('0.0.0.0:8000') 13 | 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "student_explorer", 3 | "version": "2.8.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/tl-its-umich-edu/student_explorer.git" 7 | }, 8 | "license": "Apache-2.0", 9 | "dependencies": { 10 | "bootstrap": "3.4.1", 11 | "components-font-awesome": "5.9.0", 12 | "d3": "3.5.17", 13 | "jquery": "3.6.0", 14 | "nvd3": "1.1.15-beta2", 15 | "tablesorter": "2.31.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /seumich/static/seumich/styles/_secolor.scss: -------------------------------------------------------------------------------- 1 | $color_active_gray: rgb(204, 204, 204); 2 | $color_hover_gray: rgb(229, 229, 229); 3 | $color_background_gray: rgb(249, 249, 249); 4 | $color_engage_red: rgb(193, 39, 45); 5 | $color_student_detail_background: rgb(62, 108, 156); 6 | $color_nav_background: rgb(28, 49, 68); 7 | $color_nav_hover: rgb(18, 68, 101); 8 | $color_lms_activity: rgb(190, 205, 192); 9 | $color_student_average: rgb(62, 108, 156); 10 | $color_course_average: rgb(244, 224, 117); 11 | -------------------------------------------------------------------------------- /management/migrations/0004_auto_20200302_1349.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.10 on 2020-03-02 18:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('management', '0003_mysql_cache'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cohort', 15 | name='description', 16 | field=models.CharField(max_length=50), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /student_explorer/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for foobar project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 15 | 'student_explorer.settings') 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /student_explorer/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block title %} 4 | Page Not Found 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

Not found

10 |

Sorry, but the page you were trying to view does not exist.

11 |

It looks like this was the result of either:

12 | 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /seumich/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | 4 | 5 | Student Explorer 6 | 7 |
8 |
9 |
10 | 13 | {% block content %}{% endblock %} 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /seumich/static/seumich/index.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Handle old URLs based on hash fragment routing 3 | var hash = window.location.hash; 4 | if (hash.length > 0) { 5 | var url = hash.substring(1); 6 | console.log("Hash fragmnet found, redirecting to: " + url); 7 | window.location = url; 8 | 9 | } 10 | 11 | $('[data-toggle="tooltip"]').tooltip(); 12 | 13 | $('.pagination .disabled a, .pagination .active a').on('click', function(e) { 14 | e.preventDefault(); 15 | }); 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Student Explorer Docker Django", 6 | "type": "python", 7 | "request": "attach", 8 | "pathMappings": [ 9 | { 10 | "localRoot": "${workspaceFolder}", 11 | "remoteRoot": "/usr/src/app" 12 | } 13 | ], 14 | "port": 3000, 15 | "host": "localhost", 16 | "justMyCode": false 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /start-localhost.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # These are only needed for dev 4 | echo "Running on Dev, setting up some test data" 5 | while ! nc -z ${DJANGO_DB_HOST} ${DJANGO_DB_PORT}; do 6 | sleep 1 # wait 1 second before check again 7 | done 8 | 9 | python manage.py migrate 10 | 11 | python manage.py loaddata student_explorer/fixtures/dev_users.json 12 | mysql -h ${DJANGO_DB_HOST} -u ${DJANGO_DB_USER} -p${DJANGO_DB_PASSWORD} ${DJANGO_DB_NAME} < seumich/fixtures/dev_data_drop_create_and_insert.sql 13 | python manage.py import_manage_fixtures 14 | 15 | . start.sh 16 | -------------------------------------------------------------------------------- /student_explorer/management/commands/import_manage_fixtures.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | 3 | from management import import_fixtures 4 | 5 | class Command(BaseCommand): 6 | help = 'Imports fixture data for the management application' 7 | 8 | def handle(self, *args, **options): 9 | try: 10 | import_fixtures.main() 11 | self.stdout.write(self.style.SUCCESS('Import of management fixtures succeeded')) 12 | except Exception: 13 | raise CommandError('Import of management fixtures failed') 14 | 15 | -------------------------------------------------------------------------------- /management/templates/management/user_add.html: -------------------------------------------------------------------------------- 1 | {% extends 'management/base.html' %} 2 | 3 | {% block management %} 4 | 5 |
{% csrf_token %} 6 | {% for field in form %} 7 |
8 | {{ field.label_tag }} 9 | {{ field.errors }} 10 | {{ field }} 11 |
12 | {% endfor %} 13 | 14 |
15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /seumich/routers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | class SeumichRouter(object): 4 | def db_for_read(self, model, **hints): 5 | if model._meta.app_label == 'seumich': 6 | return 'seumich' 7 | return None 8 | 9 | def db_for_write(self, model, **hints): 10 | if model._meta.app_label == 'seumich': 11 | return 'seumich' 12 | return None 13 | 14 | def allow_migrate(self, db, app_label, model_name=None, **hints): 15 | if app_label == 'seumich': 16 | return settings.DATABASES['seumich'].get('MIGRATE', False) 17 | -------------------------------------------------------------------------------- /student_explorer/common/db_util.py: -------------------------------------------------------------------------------- 1 | # Some utility functions used by other classes in this project 2 | 3 | import django 4 | import logging 5 | from datetime import datetime 6 | from dateutil.parser import parse 7 | from seumich.models import (LearningAnalyticsStats,) 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | def get_data_date(): 12 | try: 13 | c = LearningAnalyticsStats.objects.get(dw_data_nm='UDW Daily Tables') 14 | return {'data_time': c.load_dt, 15 | 'data_schema': c.dw_ownr_nm} 16 | except LearningAnalyticsStats.DoesNotExist: 17 | return {'data_time': datetime.min, 18 | 'data_schema': 'N/A'} 19 | -------------------------------------------------------------------------------- /tracking/eventnames.py: -------------------------------------------------------------------------------- 1 | class EventNames(object): 2 | UserCreated = 'UserCreated' 3 | UserInviteEmailSent = 'UserInviteEmailSent' 4 | UserEmailVerified = 'UserEmailVerified' 5 | UserPasswordChanged = 'UserPasswordChanged' 6 | UserEmailSent = 'UserEmailSent' 7 | UserConsented = 'UserConsented' 8 | UserActivated = 'UserActivated' 9 | UserWithdrawn = 'UserWithdrawn' 10 | UserReset = 'UserReset' 11 | UserLoggedIn = 'UserLoggedIn' 12 | UserLoggedOut = 'UserLoggedOut' 13 | PageViewed = 'PageViewed' 14 | PageError = 'PageError' 15 | Redirected = 'Redirected' 16 | Searched = 'Searched' 17 | CohortDeleted = 'CohortDeleted' 18 | -------------------------------------------------------------------------------- /management/templates/management/cohort_add.html: -------------------------------------------------------------------------------- 1 | {% extends 'management/base.html' %} 2 | 3 | {% block management %} 4 | 5 |
{% csrf_token %} 6 | {% for field in form %} 7 |
8 | {{ field.label_tag }} 9 | {{ field.errors }} 10 |

11 | {{ field.help_text }} 12 |

13 | {{ field }} 14 |
15 | {% endfor %} 16 | 17 |
18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /tracking/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.views.generic import View 4 | from django.http import HttpResponse 5 | 6 | from tracking import create_event 7 | 8 | class RecordEventView(View): 9 | eventname_param = 'name' 10 | eventnote_param = 'note' 11 | 12 | def get(self, request, *args, **kwargs): 13 | event = create_event(user=request.user, request=request, 14 | name=request.GET.get(self.eventname_param), 15 | note=request.GET.get(self.eventnote_param)) 16 | return HttpResponse(json.dumps({'eventname':event.name,'status':'saved'}), content_type='application/json') 17 | 18 | record_event = RecordEventView.as_view() -------------------------------------------------------------------------------- /seumich/templates/seumich/student_info_partial.html: -------------------------------------------------------------------------------- 1 | {% if link_present %} 2 | 3 |

{{student.first_name}} 4 | {{student.last_name}}

5 |
6 | {% else %} 7 |

{{student.first_name}} 8 | {{student.last_name}}

9 | {% endif %} 10 | 11 |

Student ID: 12 | {{ student.univ_id }}

13 |

14 | {{student.email_address}} 15 |

16 | -------------------------------------------------------------------------------- /student_explorer/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import auth 2 | from django.shortcuts import redirect, render 3 | from django.conf import settings 4 | import os 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def logout(request): 11 | logger.info('User %s logging out.' % request.user.username) 12 | auth.logout(request) 13 | return redirect(settings.LOGOUT_REDIRECT_URL) 14 | 15 | 16 | def about(request): 17 | context = {'build_name': os.getenv('OPENSHIFT_BUILD_NAME'), 18 | 'build_commit': os.getenv('OPENSHIFT_BUILD_COMMIT'), 19 | 'build_reference': os.getenv('OPENSHIFT_BUILD_REFERENCE')} 20 | return render(request, 'student_explorer/about.html', context) 21 | -------------------------------------------------------------------------------- /seumich/static/seumich/styles/_semixins.scss: -------------------------------------------------------------------------------- 1 | $tablet-width: 992px; 2 | $chart-width: 1150px; 3 | @mixin mobilemenu { 4 | position: static; 5 | left: auto; 6 | width: auto; 7 | margin: -$primary-margin -15px 0; 8 | } 9 | @mixin desktop { 10 | @media screen and (min-width: #{$tablet-width + 1}){ 11 | @content; 12 | } 13 | } 14 | @mixin mobile { 15 | @media screen and (max-width: #{$tablet-width}){ 16 | @content; 17 | } 18 | } 19 | @mixin medium { 20 | @media screen and (min-width: 768px) and (max-width: 992px){ 21 | @content; 22 | } 23 | } 24 | @mixin font ($size: null, $color: null, $weight: null) { 25 | font-size: $size; 26 | color: $color; 27 | font-weight: $weight; 28 | } 29 | -------------------------------------------------------------------------------- /seumich/static/seumich/styles/_sefont.scss: -------------------------------------------------------------------------------- 1 | $font_links_lists: (size: 14px, color: rgb(48, 114, 171)); 2 | $font_advisor_h1: (size: 36px, color: rgb(165, 42, 42)); 3 | $font_advisor_h2: (size: 24px, color: rgb(51, 51, 51)); 4 | $font_student_h1: (size: 28px, color: rgb(255, 255, 255), weight: 400); 5 | $font_student_h2: (size: 24px, color: rgb(255, 255, 255), weight: 500); 6 | $font_student_h3: (size: 24px, color: rgb(51, 51, 51), weight: 500); 7 | $font_student_h4: (size: 20px, color: rgb(51, 51, 51), weight: 500); 8 | $font_student_course_list: (size: 18px, color: rgb(44, 113, 173), weight: 500); 9 | $font_student_course_list_menu: (size: 18px, color: rgb(28, 49, 68)); 10 | $font_student_email_uniquename: (size: 14px, color: rgb(255, 255, 255), weight: 300); 11 | -------------------------------------------------------------------------------- /seumich/migrations/0003_auto_20160505_1353.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-05-05 17:53 3 | 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('seumich', '0002_custom_add_composite_primary_keys'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='weeklyclasssitescore', 17 | name='score', 18 | field=models.FloatField(db_column=b'CLASS_CURR_SCR_AVG'), 19 | ), 20 | migrations.AlterField( 21 | model_name='weeklystudentclasssitescore', 22 | name='score', 23 | field=models.FloatField(db_column=b'STDNT_CURR_SCR_AVG'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=3.2.12,<3.2.99 2 | djangosaml2==1.3.6 3 | 4 | django-debug-toolbar>=3.2,<3.2.99 5 | django-extensions>=3.1.5,<3.1.99 6 | django-mysql>=4.5.0,<4.5.99 7 | 8 | # Hasn't been updated since 2016 9 | django-npm==1.0.0 10 | 11 | # This needs to be switched to debugpy 12 | django-ptvsd-debug==1.0.3 13 | django-registration-redux==2.9 14 | 15 | # This hasn't been updated since 2016 16 | django-settings-export>=1.2.1,<1.2.99 17 | django-watchman>=1.2.0,<1.2.99 18 | libsass>=0.21.0,<0.21.99 19 | mysqlclient>=2.1.0,<2.1.99 20 | 21 | # This needs to be switched to debugpy 22 | ptvsd>=4.3.2,<4.3.99 23 | 24 | python-dateutil>=2.8.2,<2.8.99 25 | python-decouple==3.6 26 | requests>=2.27.0,<2.27.99 27 | whitenoise>=6.0.0,<6.0.99 28 | xlrd==2.0.1 29 | 30 | # This hasn't been updated since 2017 31 | xlwt==1.3.0 32 | -------------------------------------------------------------------------------- /seumich/templates/seumich/cohort_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'seumich/advisor.html' %} 2 | {% load cache %} 3 | 4 | {% block content %} 5 | {% cache settings.CACHE_TTL cohort_detail request.get_full_path %} 6 |
7 | {% if cohort %} 8 | {% include 'seumich/student_list_partial.html' %} 9 | {% else %} 10 |
11 |
12 |

Not Found

13 |
14 |
15 | No cohort profile found. 16 |
17 |
18 | {% endif %} 19 |
20 | {% endcache %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /seumich/templates/seumich/class_site_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'seumich/advisor.html' %} 2 | {% load cache %} 3 | 4 | {% block content %} 5 | {% cache settings.CACHE_TTL class_site_detail request.get_full_path %} 6 |
7 | {% if class_site %} 8 | {% include 'seumich/student_list_partial.html' %} 9 | {% else %} 10 |
11 |
12 |

Not Found

13 |
14 |
15 | No class site profile found. 16 |
17 |
18 | {% endif %} 19 |
20 | {% endcache %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /management/migrations/0003_mysql_cache.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ('management', '0002_auto_20190108_0943'), 8 | ] 9 | 10 | operations = [ 11 | migrations.RunSQL( 12 | """ 13 | CREATE TABLE IF NOT EXISTS django_mysql_cache ( 14 | cache_key varchar(255) CHARACTER SET utf8 COLLATE utf8_bin 15 | NOT NULL PRIMARY KEY, 16 | value longblob NOT NULL, 17 | value_type char(1) CHARACTER SET latin1 COLLATE latin1_bin 18 | NOT NULL DEFAULT 'p', 19 | expires BIGINT UNSIGNED NOT NULL 20 | ); 21 | """, 22 | "DROP TABLE django_mysql_cache" 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /seumich/mixins.py: -------------------------------------------------------------------------------- 1 | class SeumichDataMixin(object): 2 | def valid_date_or_none(self, dt): 3 | if dt is None: 4 | return None; 5 | elif dt.id < 0: 6 | return None 7 | else: 8 | return dt 9 | 10 | def aggrate_relationships(self, collection, primary, relationship, 11 | relationship_plural=None): 12 | if relationship_plural is None: 13 | relationship_plural = relationship + 's' 14 | 15 | tmp = {} 16 | for entry in collection: 17 | pri = getattr(entry, primary) 18 | if pri not in list(tmp.keys()): 19 | tmp[pri] = [] 20 | rel = getattr(entry, relationship) 21 | tmp[pri].append(rel) 22 | 23 | aggrated = [] 24 | for (pri, rels) in list(tmp.items()): 25 | aggrated.append({primary: pri, relationship_plural: rels}) 26 | 27 | return aggrated 28 | -------------------------------------------------------------------------------- /seumich/static/seumich/images/se-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | SE E-Graph 9 | Created with Sketch. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /seumich/templates/seumich/student_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'seumich/student.html' %} 2 | {% load cache %} 3 | 4 | {% block content %} 5 | {% cache settings.CACHE_TTL student_list request.get_full_path %} 6 |
7 | {% if not students %} 8 | {% if user.is_authenticated %} 9 |
10 |
11 |

No Results

12 |
13 |
14 | No students found that match the query terms. 15 |
16 |
17 | {% endif %} 18 | {% else %} 19 | {% include 'seumich/student_list_partial.html' %} 20 | {% endif %} 21 |
22 | {% endcache %} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /management/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'management' 6 | urlpatterns = [ 7 | path('', views.IndexView.as_view(), name='index'), 8 | path('users/', views.UserListView.as_view(), name='user-list'), 9 | path('cohorts/', views.CohortListView.as_view(), name='cohort-list'), 10 | path('cohorts/add/', views.AddCohortView.as_view(), name='add-cohort'), 11 | path('users/add/', views.AddUserView.as_view(), name='add-user'), 12 | path('cohorts/download/', views.CohortListDownloadView.as_view(), name='cohort-list-download'), 13 | path('cohorts/detail/download/', views.CohortDetailDownloadView.as_view(), name='cohort-detail-download'), 14 | path( 15 | 'cohorts//members/download/', 16 | views.CohortMembersDownloadView.as_view(), 17 | name='cohort-members-download' 18 | ), 19 | path('cohorts//members/', views.CohortMembersView.as_view(), name='cohort-members'), 20 | ] 21 | -------------------------------------------------------------------------------- /tracking/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.dispatch import receiver 4 | from django.contrib.auth.signals import user_logged_in, user_logged_out 5 | 6 | from tracking.eventnames import EventNames 7 | 8 | def _get_user(kwargs): 9 | user = kwargs.get('user') 10 | if user is None: 11 | user = getattr(kwargs.get('request'), 'user', None) 12 | return user 13 | 14 | @receiver(user_logged_in) 15 | def user_logged_in_callback(sender, **kwargs): 16 | from tracking.utils import create_event 17 | user = _get_user(kwargs) 18 | if user is not None: 19 | create_event(EventNames.UserLoggedIn, user=user, 20 | request=kwargs.get('request')) 21 | 22 | @receiver(user_logged_out) 23 | def user_logged_out_callback(sender, **kwargs): 24 | from tracking.utils import create_event 25 | user = _get_user(kwargs) 26 | if user is not None: 27 | create_event(EventNames.UserLoggedOut, user=user, 28 | request=kwargs.get('request')) 29 | -------------------------------------------------------------------------------- /student_explorer/management/commands/createuser.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from django.contrib.auth import get_user_model 3 | # from django.db.utils import IntegrityError 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Adds a user with an unusable password' 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument('username', nargs='+', type=str) 11 | 12 | def handle(self, *args, **options): 13 | User = get_user_model() 14 | for username in options['username']: 15 | try: 16 | user = User.objects.create_user(username, password=None) 17 | except Exception as e: 18 | raise CommandError( 19 | 'User "%s" could not be created ("%s")' % (username, e)) 20 | # user.set_unusable_password() 21 | # user.save() 22 | 23 | self.stdout.write(self.style.SUCCESS( 24 | 'Created user "%s"' % user.username)) 25 | -------------------------------------------------------------------------------- /seumich/templates/seumich/advisor_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'seumich/advisor.html' %} 2 | {% load cache %} 3 | 4 | {% block content %} 5 | {% cache settings.CACHE_TTL advisor_detail request.get_full_path %} 6 |
7 | {% if advisor %} 8 | {% if not students %} 9 | {% include 'seumich/advisor_without_students.html' %} 10 | {% else %} 11 | {% include 'seumich/student_list_partial.html' %} 12 | {% endif %} 13 | {% else %} 14 |
15 |
16 |

Not Found

17 |
18 |
19 | No advisor profile found. 20 |
21 |
22 | {% endif %} 23 |
24 | {% endcache %} 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /seumich/static/seumich/sort_table.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $(".table").tablesorter({ 3 | theme: "bootstrap", 4 | headerTemplate: "{content} {icon}", 5 | widgets: [ "uitheme", "filter", "zebra", "saveSort" ], 6 | widgetOptions: { 7 | saveSort: true 8 | } 9 | }) 10 | .tablesorterPager({ 11 | // target the pager markup - see the HTML block below 12 | container: $(".ts-pager"), 13 | // target the pager page select dropdown - choose a page 14 | cssGoto : ".pagenum", 15 | // remove rows from the table to speed up the sort of large tables. 16 | // setting this to false, only hides the non-visible rows; needed if you plan to add/remove rows with the pager enabled. 17 | removeRows: true, 18 | // output string - default is '{page}/{totalPages}'; 19 | // possible variables: {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} 20 | output: "Page / Total Page(s) : {page} / {totalPages}" 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /management/migrations/0002_auto_20190108_0943.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.18 on 2019-01-08 14:43 3 | 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('management', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='MentorStudentCourseObserver', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('student_id', models.CharField(max_length=50)), 20 | ('mentor_uniqname', models.CharField(max_length=50)), 21 | ('course_id', models.CharField(max_length=50)), 22 | ('course_section_id', models.CharField(max_length=50)), 23 | ], 24 | ), 25 | migrations.AlterUniqueTogether( 26 | name='mentorstudentcourseobserver', 27 | unique_together=set([('student_id', 'mentor_uniqname', 'course_id')]), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /management/templates/management/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block content %} 4 | 26 |
27 |
28 | 35 |
36 | {% block management %}{% endblock %} 37 |
38 |
39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /management/templates/management/cohort_members.html: -------------------------------------------------------------------------------- 1 | {% extends 'management/base.html' %} 2 | 3 | {% block management %} 4 | 5 | 6 | Download StudentCohortMentor_{{ cohort_code }}.xls 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for object in object_list %} 19 | 20 | 21 | 22 | 23 | 24 | {% empty %} 25 | 26 | 27 | 28 | {% endfor %} 29 | 30 |
Student UsernameCohort CodeAdvisor Username
{{ object.student.username }}{{ object.cohort.code }}{{ object.mentor.username }}
None Found
31 | {% include 'management/pagination.html' %} 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /seumich/static/seumich/assignment_list_table.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | $('img[id^=plus-button-]').show(); 4 | $('img[id^=minus-button-]').hide(); 5 | $('p[id^=assignment-grader-comment-]').hide(); 6 | 7 | 8 | $('img[id^=plus-button-]').on('click', function() { 9 | var imgId; 10 | 11 | imgId = $(this).attr('id').split('-').pop(); 12 | // hide plus button 13 | $(this).hide(); 14 | // show grader comment 15 | $('#assignment-grader-comment-' + imgId).show(); 16 | // change text of comment title 17 | $('#comment-title-' + imgId).text('Hide Comment'); 18 | // show minus button 19 | $('#minus-button-' + imgId).show(); 20 | }) 21 | 22 | $('img[id^=minus-button-]').on('click', function() { 23 | var imgId; 24 | 25 | imgId = $(this).attr('id').split('-').pop(); 26 | // hide minus button 27 | $(this).hide(); 28 | // hide grader comment 29 | $('#assignment-grader-comment-' + imgId).hide(); 30 | // change text of comment title 31 | $('#comment-title-' + imgId).text('View Comment'); 32 | // show plus button 33 | $('#plus-button-' + imgId).show(); 34 | }) 35 | 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /seumich/templates/seumich/cohort_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'seumich/advisor.html' %} 2 | 3 | {% block head %} 4 | {% load static %} 5 | 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 |
11 |

All Cohorts

12 |
13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | {% for cohort in cohorts %} 23 | 24 | 27 | 28 | {% endfor %} 29 | 30 |
17 | Name 18 |
25 | {{ cohort.description }} 26 |
31 |
32 |
33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /seumich/templates/seumich/ts_pager.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 5 | 7 |
8 | 9 |
10 | 12 | 14 |
15 | 21 | 22 | -------------------------------------------------------------------------------- /student_explorer/templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block title %} 4 | Forbidden 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

Permission Denied

10 |

You do not currently have access to Student Explorer.

11 |

What is Student Explorer?

12 |

To learn about Student Explorer and its history, see 13 | About Student Explorer. For more information 14 | about using Student Explorer, see 15 | Student 16 | Explorer Resources & Support.

17 |

How do I request access to Student Explorer?

18 | 24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /student_explorer/templates/405.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block title %} 4 | Forbidden 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

Permission Denied

10 |

You do not currently have access to Student Explorer.

11 |

What is Student Explorer?

12 |

To learn about Student Explorer and its history, see 13 | About Student Explorer. For more information 14 | about using Student Explorer, see 15 | Student 16 | Explorer Resources & Support.

17 |

How do I request access to Student Explorer?

18 | 24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /feedback/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-06-17 11:43 3 | 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django_extensions.db.fields 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Feedback', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), 25 | ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), 26 | ('feedback_message', models.TextField()), 27 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 28 | ], 29 | options={ 30 | 'ordering': ('-modified', '-created'), 31 | 'abstract': False, 32 | 'get_latest_by': 'modified', 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /tracking/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from django.db import migrations, models 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('contenttypes', '0002_remove_content_type_name'), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Event', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 20 | ('name', models.CharField(max_length=100)), 21 | ('timestamp', models.DateTimeField(auto_now_add=True)), 22 | ('note', models.CharField(default='', max_length=255)), 23 | ('related_object_id', models.PositiveIntegerField(null=True, blank=True)), 24 | ('related_content_type', models.ForeignKey(on_delete=models.CASCADE, blank=True, to='contenttypes.ContentType', null=True)), 25 | ('user', models.ForeignKey(on_delete=models.CASCADE, blank=True, to=settings.AUTH_USER_MODEL, null=True)), 26 | ], 27 | options={ 28 | 'ordering': ('-timestamp',), 29 | 'get_latest_by': 'timestamp', 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo $DJANGO_SETTINGS_MODULE 4 | 5 | if [ -z "${GUNICORN_WORKERS}" ]; then 6 | GUNICORN_WORKERS=4 7 | fi 8 | 9 | if [ -z "${GUNICORN_PORT}" ]; then 10 | GUNICORN_PORT=8000 11 | fi 12 | 13 | if [ -z "${GUNICORN_TIMEOUT}" ]; then 14 | GUNICORN_TIMEOUT=120 15 | fi 16 | 17 | if [ "${GUNICORN_RELOAD}" ]; then 18 | GUNICORN_RELOAD="--reload" 19 | else 20 | GUNICORN_RELOAD="" 21 | fi 22 | 23 | set -x 24 | 25 | python manage.py migrate 26 | 27 | if [ "${CACHE_BACKEND:-""}" == "django.core.cache.backends.db.DatabaseCache" ]; then 28 | echo "Database cache set; creating cache table" 29 | python manage.py createcachetable 30 | fi 31 | 32 | if [ "${PTVSD_ENABLE:-"False"}" == "False" ]; then 33 | # Start Gunicorn processes 34 | echo Starting Gunicorn for production 35 | 36 | # application pod 37 | exec gunicorn student_explorer.wsgi:application \ 38 | --bind 0.0.0.0:${GUNICORN_PORT} \ 39 | --workers="${GUNICORN_WORKERS}" \ 40 | --timeout="${GUNICORN_TIMEOUT}" \ 41 | ${GUNICORN_RELOAD} 42 | else 43 | # Currently ptvsd doesn't work with gunicorn 44 | # https://github.com/Microsoft/vscode-python/issues/2138 45 | echo Starting Runserver for development 46 | export PYTHONPATH="/usr/src/app:$PYTHONPATH" 47 | export DJANGO_SETTINGS_MODULE=student_explorer.settings 48 | exec django-admin runserver --ptvsd 0.0.0.0:${GUNICORN_PORT} 49 | fi -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mysql: 5 | image: mysql:8-oracle 6 | restart: on-failure 7 | environment: 8 | - MYSQL_HOST=${DJANGO_DB_HOST} 9 | - MYSQL_DATABASE=${DJANGO_DB_NAME} 10 | - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} 11 | - MYSQL_USER=${DJANGO_DB_USER} 12 | - MYSQL_PASSWORD=${DJANGO_DB_PASSWORD} 13 | - MYSQL_PORT=${DJANGO_DB_PORT} 14 | command: ['--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci', '--socket=/tmp/mysql.sock'] 15 | entrypoint: ['docker-entrypoint.sh', '--default-authentication-plugin=mysql_native_password'] 16 | ports: 17 | - "2034:3306" 18 | volumes: 19 | - ./mysql:/docker-entrypoint-initdb.d/:ro 20 | container_name: student_explorer_mysql 21 | web: 22 | build: 23 | context: . 24 | dockerfile: Dockerfile 25 | args: 26 | - LOCALHOST_DEV=1 27 | command: bash -c "./start-localhost.sh" 28 | volumes: 29 | - .:/usr/src/app 30 | # use the container's static folder (don't override) 31 | - /usr/src/app/staticfiles 32 | - /usr/src/app/seumich/static 33 | - /usr/src/app/student_explorer/local 34 | ports: 35 | - "2082:8000" 36 | - "3000:3000" 37 | depends_on: 38 | - mysql 39 | env_file: 40 | - .env 41 | environment: 42 | - LOCALHOST_DEV=1 43 | - GUNICORN_RELOAD=True 44 | container_name: student_explorer 45 | -------------------------------------------------------------------------------- /seumich/templatetags/filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.safestring import mark_safe 3 | import json 4 | import decimal 5 | 6 | register = template.Library() 7 | 8 | # Source: 9 | # http://stackoverflow.com/questions/16957275/python-to-json-serialization-fails-on-decimal 10 | 11 | 12 | def decimal_default(obj): 13 | if isinstance(obj, decimal.Decimal): 14 | return float(obj) 15 | raise TypeError 16 | 17 | 18 | @register.filter 19 | def get_score(qs): 20 | if qs.exists(): 21 | avg_score = qs[0].current_score_average 22 | if avg_score is None: 23 | return 'N/A' 24 | else: 25 | return avg_score 26 | else: 27 | return 'N/A' 28 | 29 | 30 | @register.filter 31 | def jsonify(list): 32 | return mark_safe(json.dumps(list, default=decimal_default)) 33 | 34 | 35 | @register.filter 36 | def divide(value, arg): 37 | try: 38 | return float(value) / float(arg) 39 | except: 40 | return None 41 | 42 | 43 | @register.filter 44 | def multiply(value, arg): 45 | try: 46 | return float(value) * float(arg) 47 | except: 48 | return value 49 | 50 | 51 | @register.filter 52 | def get_bar_width(value, arg): 53 | width = value 54 | if arg != 'N/A' and value != 'N/A': 55 | arg = float(arg) 56 | value = float(value) 57 | if arg > 100.0 and value < arg: 58 | width = value * 100.0 / arg 59 | return width 60 | -------------------------------------------------------------------------------- /seumich/urls.py: -------------------------------------------------------------------------------- 1 | """student_explorer URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.urls import path 17 | from seumich import views 18 | 19 | app_name = 'seumich' 20 | 21 | urlpatterns = [ 22 | path('', views.IndexView.as_view(), name='index'), 23 | path('advisors/', views.AdvisorsListView.as_view(), name='advisors_list'), 24 | path('cohorts/', views.CohortsListView.as_view(), name='cohorts_list'), 25 | path('advisors//', views.AdvisorView.as_view(), name='advisor'), 26 | path('cohorts//', views.CohortView.as_view(), name='cohort'), 27 | path('classes//', views.ClassSiteView.as_view(), name='class_site'), 28 | path('students/', views.StudentsListView.as_view(), name='students_list'), 29 | path('students//', views.StudentView.as_view(), name='student'), 30 | path('students//class_sites//', views.StudentClassSiteView.as_view(), name='student_class'), 31 | ] 32 | -------------------------------------------------------------------------------- /feedback/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from feedback.forms import FeedbackForm 3 | from django.http import HttpResponseRedirect 4 | from django.core.mail import send_mail 5 | from django.conf import settings 6 | from feedback.models import Feedback 7 | from django.contrib.auth.decorators import login_required 8 | from django.contrib import messages 9 | 10 | 11 | @login_required 12 | def submitFeedback(request): 13 | if request.method == 'POST': 14 | form = FeedbackForm(request.POST) 15 | if form.is_valid(): 16 | user = request.user 17 | feedback_message = form.cleaned_data['feedback_message'] 18 | 19 | feedback = Feedback.objects.create( 20 | user=user, feedback_message=feedback_message) 21 | 22 | feedback_message_email = "From: %s <%s>\nFeedback:\n\n%s" % ( 23 | user.get_full_name(), user.email, feedback_message) 24 | send_mail('Student Explorer Feedback (%s, id: %s)' % ( 25 | user.email, feedback.id), 26 | feedback_message_email, settings.FEEDBACK_EMAIL, 27 | (settings.FEEDBACK_EMAIL,), 28 | fail_silently=False, 29 | ) 30 | messages.add_message( 31 | request, 32 | messages.SUCCESS, 33 | 'Thank you for submitting your feedback!') 34 | return HttpResponseRedirect('/') 35 | else: 36 | form = FeedbackForm() 37 | 38 | return render( 39 | request, 40 | 'feedback/feedback.html', 41 | {'form': form} 42 | ) 43 | -------------------------------------------------------------------------------- /student_explorer/middleware.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import logging 3 | from django.http import HttpResponseNotAllowed 4 | from django.template.loader import render_to_string 5 | try: 6 | from django.utils.deprecation import MiddlewareMixin 7 | except ImportError: 8 | MiddlewareMixin = object 9 | 10 | logger = logging.getLogger('access_logs') 11 | 12 | 13 | class LoggingMiddleware(MiddlewareMixin): 14 | def process_response(self, request, response): 15 | l = [] 16 | 17 | l.append(request.META.get( 18 | 'HTTP_X_FORWARDED_FOR', 19 | request.META.get('REMOTE_ADDR', '-') 20 | ) 21 | ) 22 | 23 | if hasattr(request, 'user') and request.user.is_authenticated: 24 | l.append(request.user.username) 25 | else: 26 | l.append('-') 27 | 28 | l.append(datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S')) 29 | 30 | l.append('"' + request.META.get('REQUEST_METHOD', '-') + ' ' + 31 | request.get_full_path() + '"') 32 | 33 | l.append(str(response.status_code)) 34 | 35 | l.append('"' + request.META.get('HTTP_REFERER', '-') + '"') 36 | 37 | l.append('"' + request.META.get('HTTP_USER_AGENT', '-') + '"') 38 | 39 | logger.info(' '.join(l)) 40 | return response 41 | 42 | class HttpResourceNotAllowedMiddleware(MiddlewareMixin): 43 | def process_response(self, request, response): 44 | if isinstance(response, HttpResponseNotAllowed): 45 | response.content = render_to_string("405.html", request=request) 46 | return response 47 | -------------------------------------------------------------------------------- /student_explorer/backends.py: -------------------------------------------------------------------------------- 1 | from djangosaml2.backends import Saml2Backend 2 | from django.core.exceptions import PermissionDenied 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class ActiveUserOnlySAML2Backend(Saml2Backend): 10 | def authenticate( 11 | self, 12 | request, 13 | **kwargs, 14 | ): 15 | user = None 16 | try: 17 | user = super(ActiveUserOnlySAML2Backend, self).authenticate(request, **kwargs) 18 | except Exception: 19 | # If there's any exception with this authenticate just return PermisisonDenied 20 | logger.exception("Exception thrown from authenticate") 21 | raise PermissionDenied 22 | # If the user returned is None then we should also give raise PermissionDenied 23 | if not user: 24 | raise PermissionDenied 25 | # The user should be made active if they exist and aren't active 26 | if not user.is_active: 27 | user.is_active = True 28 | user.save() 29 | return user 30 | 31 | 32 | def is_authorized( 33 | self, 34 | attributes: dict, 35 | attribute_mapping: dict, 36 | idp_entityid: str, 37 | assertion_info: dict, 38 | **kwargs, 39 | ) -> bool: 40 | # If there are any groups that we're a member of, return true 41 | # These groups are controlled via request to Shibboleth team INC1715416 42 | if attributes.get('isMemberOf'): 43 | logger.debug(attributes.get('isMemberOf')) 44 | return True 45 | else: 46 | logger.warning('The user "%s" is not in one of the allowed groups', attributes.get('uid')) 47 | logger.warning(attributes) 48 | return False 49 | -------------------------------------------------------------------------------- /seumich/static/seumich/images/feedback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /feedback/templates/feedback/feedback.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
{% csrf_token %} 8 |

Feedback

9 | Please tell us about your experience using Student Explorer. Your feedback will help us improve the tool. 10 |
11 | 12 | 13 |
14 | 15 |
16 | {% for error in form.errors.feedback_message %} 17 | 20 | {% endfor %} 21 | 22 |
23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /seumich/migrations/0002_custom_add_composite_primary_keys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-05-02 18:58 3 | 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('seumich', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RunSQL(['ALTER TABLE `"CNLYR002"."BG_STDNT_ADVSR_ROLE"` DROP PRIMARY KEY, ADD PRIMARY KEY (`STDNT_KEY`, `ADVSR_KEY`, `ADVSR_ROLE_KEY`);']), 17 | migrations.RunSQL(['ALTER TABLE `"CNLYR002"."BG_STDNT_CHRT_MNTR"` DROP PRIMARY KEY, ADD PRIMARY KEY (`STDNT_KEY`, `MNTR_KEY`, `CHRT_KEY`);']), 18 | migrations.RunSQL(['ALTER TABLE `"CNLYR002"."FC_STDNT_CLASS_SCR"` DROP PRIMARY KEY, ADD PRIMARY KEY (`STDNT_KEY`, `CLASS_SITE_KEY`);']), 19 | migrations.RunSQL(['ALTER TABLE `"CNLYR002"."FC_STDNT_CLASS_ASSGN"` DROP PRIMARY KEY, ADD PRIMARY KEY (`STDNT_KEY`, `CLASS_SITE_KEY`, `ASSGN_KEY`);']), 20 | migrations.RunSQL(['ALTER TABLE `"CNLYR002"."FC_STDNT_CLASS_ACAD_PERF"` DROP PRIMARY KEY, ADD PRIMARY KEY (`STDNT_KEY`, `CLASS_SITE_KEY`, `ACAD_PERF_KEY`);']), 21 | migrations.RunSQL(['ALTER TABLE `"CNLYR002"."FC_CLASS_WKLY_SCR"` DROP PRIMARY KEY, ADD PRIMARY KEY (`CLASS_SITE_KEY`, `WEEK_END_DT_KEY`);']), 22 | migrations.RunSQL(['ALTER TABLE `"CNLYR002"."FC_STDNT_CLASS_WKLY_EVENT"` DROP PRIMARY KEY, ADD PRIMARY KEY (`STDNT_KEY`, `CLASS_SITE_KEY`, `WEEK_END_DT_KEY`);']), 23 | migrations.RunSQL(['ALTER TABLE `"CNLYR002"."FC_STDNT_CLASS_WKLY_SCR"` DROP PRIMARY KEY, ADD PRIMARY KEY (`STDNT_KEY`, `CLASS_SITE_KEY`, `WEEK_END_DT_KEY`);']), 24 | migrations.RunSQL(['ALTER TABLE `"CNLYR002"."FC_STDNT_CLS_WKLY_ACAD_PRF"` DROP PRIMARY KEY, ADD PRIMARY KEY (`STDNT_KEY`, `CLASS_SITE_KEY`, `WEEK_END_DT_KEY`, `ACAD_PERF_KEY`);']), 25 | ] 26 | -------------------------------------------------------------------------------- /management/import_fixtures.py: -------------------------------------------------------------------------------- 1 | import json, os, sys 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | sys.path.append(ROOT_DIR) 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'student_explorer.settings') 8 | 9 | application = get_wsgi_application() 10 | 11 | from management.models import Student, Cohort, Mentor, StudentCohortMentor 12 | 13 | FIXTURES_PATH = os.path.join('management', 'fixtures') 14 | 15 | 16 | def main(): 17 | 18 | # Opening JSON fixture files with fake management data 19 | with open(os.path.join(FIXTURES_PATH, 'cohorts.json'), encoding='utf8') as cohorts_file: 20 | cohorts = json.loads(cohorts_file.read()) 21 | 22 | with open(os.path.join(FIXTURES_PATH, 'student_mentor_mappings.json'), encoding='utf8') as student_mentor_file: 23 | student_mentor_mappings = json.loads(student_mentor_file.read()) 24 | 25 | for cohort in cohorts: 26 | Cohort.objects.get_or_create( 27 | code=cohort['code'].strip(), 28 | description=cohort['description'].strip(), 29 | group=cohort['group'].strip(), 30 | active=True 31 | ) 32 | 33 | for student_mentor_mapping in student_mentor_mappings: 34 | student_id = student_mentor_mapping['student_id'].strip() 35 | code = student_mentor_mapping['code'].strip() 36 | mentor_id = student_mentor_mapping['mentor_id'].strip() 37 | 38 | cohort = Cohort.objects.get(code=code) 39 | student = Student.objects.get_or_create(username=student_id)[0] 40 | mentor = Mentor.objects.get_or_create(username=mentor_id)[0] 41 | StudentCohortMentor.objects.get_or_create( 42 | student=student, 43 | cohort=cohort, 44 | mentor=mentor 45 | ) 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /student_explorer/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | student_explorer URL Configuration 3 | 4 | This file was originally created to work with Django 1.8. It has since been updated to use Django 2.2. 5 | Refer to the Django documentation at the following link for proper usage: 6 | https://docs.djangoproject.com/en/2.2/ref/urls/ 7 | 8 | """ 9 | from django.urls import include, path 10 | from django.views.generic import TemplateView 11 | from django.contrib import admin 12 | from django.conf import settings 13 | from . import views 14 | 15 | urlpatterns = [ 16 | path( 17 | 'robots.txt', 18 | TemplateView.as_view(template_name="robots.txt", content_type="text/plain"), 19 | name="robots_file" 20 | ), 21 | path('', include('seumich.urls', namespace='seumich')), 22 | path('about', views.about, name='about'), 23 | path('admin/', admin.site.urls), 24 | path('manage/', include('management.urls')), 25 | path('feedback/', include('feedback.urls', namespace='feedback')), 26 | path('usage/', include('usage.urls', namespace='usage')), 27 | path('status/', include('watchman.urls')), 28 | path('pages/', include('django.contrib.flatpages.urls')), 29 | ] 30 | 31 | # Override auth_logout from djangosaml2 and registration for consistent behavior 32 | urlpatterns.append(path('accounts/logout/', views.logout, name='auth_logout')) 33 | 34 | if 'djangosaml2' in settings.INSTALLED_APPS: 35 | from djangosaml2.views import EchoAttributesView 36 | urlpatterns += [ 37 | path('accounts/', include('djangosaml2.urls')), 38 | path('accounts/echo_attributes/', EchoAttributesView.as_view()), 39 | ] 40 | elif 'registration' in settings.INSTALLED_APPS: 41 | urlpatterns += [ 42 | path('accounts/', include('registration.backends.default.urls')), 43 | ] 44 | 45 | # Configure Django Debug Toolbar 46 | if settings.DEBUG: 47 | import debug_toolbar 48 | urlpatterns += [ 49 | path('__debug__/', include(debug_toolbar.urls)), 50 | ] 51 | -------------------------------------------------------------------------------- /student_explorer/fixtures/dev_users_profiles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "is_policy_accepted": null, 5 | "is_student": null, 6 | "policy_accepted_date": null, 7 | "user": 1, 8 | "is_faculty": null 9 | }, 10 | "model": "umichuser.profile", 11 | "pk": 1 12 | }, 13 | { 14 | "fields": { 15 | "is_policy_accepted": true, 16 | "is_student": false, 17 | "policy_accepted_date": "2015-05-18", 18 | "user": 6, 19 | "is_faculty": false 20 | }, 21 | "model": "umichuser.profile", 22 | "pk": 2 23 | }, 24 | { 25 | "fields": { 26 | "is_policy_accepted": false, 27 | "is_student": false, 28 | "policy_accepted_date": null, 29 | "user": 7, 30 | "is_faculty": false 31 | }, 32 | "model": "umichuser.profile", 33 | "pk": 3 34 | }, 35 | { 36 | "fields": { 37 | "is_policy_accepted": true, 38 | "is_student": false, 39 | "policy_accepted_date": "2015-05-18", 40 | "user": 8, 41 | "is_faculty": false 42 | }, 43 | "model": "umichuser.profile", 44 | "pk": 4 45 | }, 46 | { 47 | "fields": { 48 | "is_policy_accepted": false, 49 | "is_student": false, 50 | "policy_accepted_date": null, 51 | "user": 9, 52 | "is_faculty": false 53 | }, 54 | "model": "umichuser.profile", 55 | "pk": 5 56 | }, 57 | { 58 | "fields": { 59 | "is_policy_accepted": true, 60 | "is_student": false, 61 | "policy_accepted_date": "2015-05-18", 62 | "user": 10, 63 | "is_faculty": false 64 | }, 65 | "model": "umichuser.profile", 66 | "pk": 6 67 | }, 68 | { 69 | "fields": { 70 | "is_policy_accepted": true, 71 | "is_student": false, 72 | "policy_accepted_date": "2015-03-02", 73 | "user": 11, 74 | "is_faculty": false 75 | }, 76 | "model": "umichuser.profile", 77 | "pk": 7 78 | } 79 | ] 80 | -------------------------------------------------------------------------------- /seumich/templates/seumich/advisor_without_students.html: -------------------------------------------------------------------------------- 1 | {% block content %} 2 |

{{ studentListHeader }}

3 |
4 |

Welcome to Student Explorer!

5 |

Student Explorer helps academic advisors identify academically at-risk 6 | students using Learning Management System data. 7 |

8 |
9 |
10 | 11 |

Search for any student

12 |

Type the student's name, uniqname, or UMID in the search bar above to explore their academic 13 | performance. You can find any U-M Ann Arbor student, including graduate students, in Student 14 | Explorer. 15 |

16 |
17 |
18 | 19 |

Create your student list

20 |

Want to see a list of your students all in one place? Student Explorer lets you view a list of your 21 | students, called a "cohort," for quick and easy reference. 22 | Learn more about cohorts and 23 | how to set one up. 24 |

25 |
26 |
27 | 28 |

Learn more

29 |

Visit our About page to learn more about the purpose and history of Student 30 | Explorer, or visit Resources & Support 31 | for information on how to use Student Explorer. 32 |

33 |
34 |
35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /management/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.db import models 3 | 4 | from tracking.eventnames import EventNames 5 | from tracking.utils import create_event 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class BaseModel(models.Model): 12 | created_at = models.DateTimeField(auto_now_add=True) 13 | updated_at = models.DateTimeField(auto_now=True) 14 | 15 | class Meta: 16 | abstract = True 17 | 18 | 19 | class Mentor(BaseModel): 20 | username = models.CharField(max_length=20, primary_key=True) 21 | 22 | def save(self, *args, **kwargs): 23 | self.username = self.username.lower() 24 | super(Mentor, self).save(*args, **kwargs) 25 | 26 | 27 | class Cohort(BaseModel): 28 | code = models.CharField(max_length=50, primary_key=True) 29 | description = models.CharField(max_length=50) 30 | group = models.CharField(max_length=50) 31 | active = models.BooleanField(default=True) 32 | 33 | def delete(self, *args, **kwargs): 34 | request = kwargs.pop("request", None) 35 | super(Cohort, self).delete(*args, **kwargs) 36 | create_event(EventNames.CohortDeleted,request = request) 37 | 38 | 39 | class Student(BaseModel): 40 | username = models.CharField(max_length=20, primary_key=True) 41 | 42 | def save(self, *args, **kwargs): 43 | self.username = self.username.lower() 44 | super(Student, self).save(*args, **kwargs) 45 | 46 | 47 | class StudentCohortMentor(BaseModel): 48 | student = models.ForeignKey(Student, on_delete=models.CASCADE) 49 | cohort = models.ForeignKey(Cohort, on_delete=models.CASCADE) 50 | mentor = models.ForeignKey(Mentor, on_delete=models.CASCADE) 51 | 52 | class Meta: 53 | unique_together = ('student', 'cohort', 'mentor') 54 | 55 | class MentorStudentCourseObserver(models.Model): 56 | student_id = models.CharField(max_length=50) 57 | mentor_uniqname = models.CharField(max_length=50) 58 | course_id = models.CharField(max_length=50) 59 | course_section_id = models.CharField(max_length=50) 60 | 61 | class Meta: 62 | unique_together = ('student_id', 'mentor_uniqname', 'course_id') 63 | -------------------------------------------------------------------------------- /management/templates/management/pagination.html: -------------------------------------------------------------------------------- 1 | {% if page_obj.has_other_pages %} 2 |
3 |
    4 | {% if page_obj.has_previous %} 5 |
  • 6 | {% if query_term %} 7 | « 8 | {% else %} 9 | « 10 | {% endif %} 11 |
  • 12 | {% else %} 13 |
  • 14 | « 15 |
  • 16 | {% endif %} 17 | {% for i in page_obj.paginator.page_range %} 18 | {% if page_obj.number == i %} 19 |
  • 20 | {{ i }} 21 | (current) 22 | 23 |
  • 24 | {% else %} 25 |
  • 26 | {% if query_term %} 27 | {{ i }} 28 | {% else %} 29 | {{ i }} 30 | {% endif %} 31 |
  • 32 | {% endif %} 33 | {% endfor %} 34 | {% if page_obj.has_next %} 35 |
  • 36 | {% if query_term %} 37 | » 38 | {% else %} 39 | » 40 | {% endif %} 41 |
  • 42 | {% else %} 43 |
  • 44 | » 45 |
  • 46 | {% endif %} 47 |
48 |
49 | {% endif %} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local gitignore 2 | *.pyc 3 | .DS_Store 4 | 5 | /student_explorer/local/ 6 | !/student_explorer/local/README.md 7 | 8 | /sespa/app/static 9 | 10 | /.vagrant 11 | 12 | .coverage 13 | 14 | redis 15 | 16 | # Github gitignore 17 | # https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore 18 | # Byte-compiled / optimized / DLL files 19 | __pycache__/ 20 | *.py[cod] 21 | *$py.class 22 | 23 | # C extensions 24 | *.so 25 | 26 | # Distribution / packaging 27 | .Python 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | MANIFEST 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .nox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | .hypothesis/ 66 | .pytest_cache/ 67 | 68 | # Translations 69 | *.mo 70 | *.pot 71 | 72 | # Django stuff: 73 | *.log 74 | local_settings.py 75 | db.sqlite3 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | .python-version 99 | 100 | # celery beat schedule file 101 | celerybeat-schedule 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # IDE resources 131 | .idea 132 | 133 | # Node stuff 134 | node_modules -------------------------------------------------------------------------------- /seumich/templates/seumich/advisor_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'seumich/advisor.html' %} 2 | 3 | {% block head %} 4 | {% load static %} 5 | 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 |
11 |

All Advisors

12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for advisor in advisors %} 23 | 24 | 30 | 36 | 42 | 43 | {% endfor %} 44 | 45 | 46 | {% include 'seumich/ts_pager.html' with colspan="3" %} 47 | 48 |
First NameLast NameUniqname
25 | {% url 'seumich:advisor' advisor.username as advisor_url %} 26 | {% if advisor_url %} 27 | {{ advisor.first_name }} 28 | {% endif %} 29 | 31 | {% url 'seumich:advisor' advisor.username as advisor_url %} 32 | {% if advisor_url %} 33 | {{ advisor.last_name }} 34 | {% endif %} 35 | 37 | {% url 'seumich:advisor' advisor.username as advisor_url %} 38 | {% if advisor_url %} 39 | {{advisor.username}} 40 | {% endif %} 41 |
49 |
50 |
51 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /tracking/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.dispatch import Signal, receiver 4 | from django.db import models 5 | from django.contrib.contenttypes.models import ContentType 6 | from django.contrib.contenttypes.fields import GenericForeignKey 7 | 8 | from django.conf import settings 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | class Event(models.Model): 13 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) 14 | name = models.CharField(max_length=100) 15 | timestamp = models.DateTimeField(auto_now_add=True) 16 | note = models.CharField(max_length=255, default='') 17 | related_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True) 18 | related_object_id = models.PositiveIntegerField(null=True, blank=True) 19 | related_object = GenericForeignKey('related_content_type', 'related_object_id') 20 | 21 | class Meta: 22 | ordering = ('-timestamp',) 23 | get_latest_by = 'timestamp' 24 | app_label = 'tracking' 25 | 26 | def __str__(self): 27 | return "%s at %s" % ( self.name, self.timestamp ) 28 | 29 | @classmethod 30 | def events_related_to(cls, related_object): 31 | related_ct = ContentType.objects.get_for_model(related_object) 32 | related_pk = related_object.id 33 | 34 | return Event.objects.filter(related_content_type=related_ct, 35 | related_object_id=related_pk) 36 | 37 | 38 | event_logged = Signal(providing_args=["event"]) 39 | 40 | @receiver(models.signals.post_save, sender=Event) 41 | def event_handler(sender, instance, created=False, **kwargs): 42 | if instance is not None and created: 43 | event_logged.send_robust(sender=sender, event=instance) 44 | 45 | # logging callback 46 | @receiver(event_logged) 47 | def event_logger(sender, event=None, **kwargs): 48 | if event is not None: 49 | extra = dict(note=event.note, timestamp=event.timestamp, 50 | user=None, related_object=None, request=None) 51 | msg = ['%s event created' % event.name] 52 | if event.user is not None: 53 | extra['user'] = event.user.username 54 | msg.append('for user %s' % event.user.username) 55 | if event.related_object is not None: 56 | extra['related_object'] = event.related_object 57 | msg.append('connected to %r' % event.related_object) 58 | if hasattr(event, 'request') and event.request is not None: 59 | extra['request'] = event.request 60 | msg.append('@ %s' % event.request.path) 61 | logger.info(' '.join(msg), extra=extra) 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim-bullseye 2 | 3 | RUN apt-get update && apt-get --no-install-recommends install --yes curl 4 | 5 | RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - 6 | 7 | RUN apt-get --no-install-recommends install --yes \ 8 | libaio1 libaio-dev xmlsec1 libffi-dev libsasl2-dev \ 9 | build-essential default-libmysqlclient-dev git netcat \ 10 | nodejs 11 | 12 | WORKDIR /tmp/ 13 | 14 | COPY requirements.txt /tmp/ 15 | 16 | RUN pip install -r requirements.txt && \ 17 | pip install gunicorn 18 | 19 | # Sets the local timezone of the docker image 20 | ENV TZ=America/Detroit 21 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 22 | 23 | ARG LOCALHOST_DEV 24 | 25 | WORKDIR /usr/src/app/student_explorer/dependencies/ 26 | 27 | # This is based on here. It seems like it unfortunately may need to be manually updated 28 | # http://yum.oracle.com/repo/OracleLinux/OL7/oracle/instantclient/x86_64/index.html 29 | ENV ORACLE_CLIENT_VERSION=18.5 30 | ENV ORACLE_CLIENT_VERSION_FULL=18.5.0.0.0-3 31 | ENV ORACLE_HOME /usr/lib/oracle/$ORACLE_CLIENT_VERSION/client64 32 | ENV LD_LIBRARY_PATH /usr/lib/oracle/$ORACLE_CLIENT_VERSION/client64/lib 33 | 34 | RUN mkdir -p /usr/src/app 35 | WORKDIR /usr/src/app/ 36 | COPY . /usr/src/app 37 | 38 | WORKDIR /tmp/ 39 | # Run these only for dev 40 | # Make a python package: 41 | RUN if [ "$LOCALHOST_DEV" ] ; then \ 42 | echo "LOCALHOST_DEV is set, building development dependencies" && \ 43 | touch /usr/src/app/student_explorer/local/__init__.py && \ 44 | # Create default settings_override module: 45 | echo "from student_explorer.settings import *\n\nDEBUG = True" > /usr/src/app/student_explorer/local/settings_override.py && \ 46 | apt-get --no-install-recommends install --yes default-mysql-client && \ 47 | pip install coverage \ 48 | ; else \ 49 | echo "LOCALHOST_DEV is not set, building production (Oracle) dependencies" && \ 50 | # Converted to Debian format 51 | apt-get install --yes alien && \ 52 | curl -sO https://yum.oracle.com/repo/OracleLinux/OL7/oracle/instantclient/x86_64/getPackage/oracle-instantclient${ORACLE_CLIENT_VERSION}-basiclite-${ORACLE_CLIENT_VERSION_FULL}.x86_64.rpm && \ 53 | alien oracle-instantclient*.rpm && \ 54 | dpkg -i *.deb && rm *.deb *.rpm && \ 55 | pip install cx_Oracle==7.0 \ 56 | ; fi 57 | 58 | WORKDIR /usr/src/app/ 59 | # Compile the css file 60 | RUN pysassc seumich/static/seumich/styles/main.scss seumich/static/seumich/styles/main.css 61 | 62 | RUN npm install 63 | 64 | # This is needed to clean up the examples files as these cause collectstatic to fail (and take up extra space) 65 | RUN find node_modules -type d -name "examples" -print0 | xargs -0 rm -rf 66 | 67 | RUN python manage.py collectstatic --settings=student_explorer.settings --noinput --verbosity 0 68 | 69 | EXPOSE 8000 70 | CMD ./start.sh 71 | -------------------------------------------------------------------------------- /management/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2017-03-21 17:05 3 | 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Cohort', 19 | fields=[ 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ('code', models.CharField(max_length=50, primary_key=True, serialize=False)), 23 | ('description', models.CharField(max_length=100)), 24 | ('group', models.CharField(max_length=50)), 25 | ('active', models.BooleanField(default=True)), 26 | ], 27 | options={ 28 | 'abstract': False, 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='Mentor', 33 | fields=[ 34 | ('created_at', models.DateTimeField(auto_now_add=True)), 35 | ('updated_at', models.DateTimeField(auto_now=True)), 36 | ('username', models.CharField(max_length=20, primary_key=True, serialize=False)), 37 | ], 38 | options={ 39 | 'abstract': False, 40 | }, 41 | ), 42 | migrations.CreateModel( 43 | name='Student', 44 | fields=[ 45 | ('created_at', models.DateTimeField(auto_now_add=True)), 46 | ('updated_at', models.DateTimeField(auto_now=True)), 47 | ('username', models.CharField(max_length=20, primary_key=True, serialize=False)), 48 | ], 49 | options={ 50 | 'abstract': False, 51 | }, 52 | ), 53 | migrations.CreateModel( 54 | name='StudentCohortMentor', 55 | fields=[ 56 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 57 | ('created_at', models.DateTimeField(auto_now_add=True)), 58 | ('updated_at', models.DateTimeField(auto_now=True)), 59 | ('cohort', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='management.Cohort')), 60 | ('mentor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='management.Mentor')), 61 | ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='management.Student')), 62 | ], 63 | ), 64 | migrations.AlterUniqueTogether( 65 | name='studentcohortmentor', 66 | unique_together=set([('student', 'cohort', 'mentor')]), 67 | ), 68 | ] 69 | -------------------------------------------------------------------------------- /tracking/utils.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from django.conf import settings 4 | from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model 5 | from django.http import HttpResponseBadRequest, HttpResponse 6 | from django.contrib import messages 7 | from django.shortcuts import redirect 8 | from django.utils.decorators import method_decorator 9 | 10 | from tracking.models import Event, event_logged 11 | from tracking.eventnames import EventNames 12 | 13 | def create_event(name, request=None, user=None, note=None, related_object=None): 14 | """Make an event record for a given set of parameters. If request is 15 | given, the user is pulled from the request, and in the absence of a note, 16 | the note is set to the request path.""" 17 | if request is not None: 18 | if user is None and hasattr(request, 'user') and request.user.is_authenticated: 19 | user = request.user 20 | if note is None: 21 | note = request.path 22 | e = Event(name=name) 23 | if user is not None and user.is_authenticated: 24 | e.user = user 25 | if related_object is not None: 26 | e.related_object = related_object 27 | if note is not None: 28 | e.note = note 29 | # the following attribute is not stored in the database, but it's useful 30 | # for the logger function in tracking.models. 31 | e.request = request 32 | e.save() 33 | return e 34 | 35 | 36 | def user_log_page_view(f): 37 | @wraps(f) 38 | def wrapper(request, *args, **kwargs): 39 | user = request.user if request.user.is_authenticated else None 40 | response = f(request, *args, **kwargs) 41 | if response.status_code != 200: 42 | if response.status_code == 302: 43 | create_event(EventNames.Redirected, request, user=user, 44 | note='From: %s \tTo: %s' % (request.path, response['Location'])) 45 | else: 46 | create_event(EventNames.PageError, request, user=user, 47 | note='\n'.join([request.path, 'Response Code: %d' % response.status_code,])) 48 | else: 49 | create_event(EventNames.PageViewed, request, user=user) 50 | return response 51 | return wrapper 52 | 53 | class UserLogPageViewMixin(object): 54 | """A simple mix-in class to write an event on every request to the view. 55 | events are written using the `user_log_page_view` decorator. 56 | """ 57 | @method_decorator(user_log_page_view) 58 | def dispatch(self, request, *args, **kwargs): 59 | return super(UserLogPageViewMixin, self).dispatch(request, *args, **kwargs) 60 | 61 | class LogEventTypeMixin(object): 62 | eventname = None 63 | 64 | def log_event(self, note=None): 65 | create_event(name=self.eventname, request=self.request, 66 | user=self.request.user, note=note) 67 | -------------------------------------------------------------------------------- /usage/templates/usage.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | {% load filters %} 3 | 4 | {% block content %} 5 | 6 |
7 |

Usage Statistics

8 | {% now "Y" as current_year %} 9 | Download Users for the Academic Year 10 | {{current_year|add:-1}}-{{current_year}} 11 | 12 | {{usersCount}} 13 | 14 | 15 |

Unique Students Viewed for the Academic Year 16 | {{current_year|add:-1}}-{{current_year}} 17 | 18 | {{studentsCount}} 19 | 20 |

21 |
22 | 23 | 61 | 62 |
63 |

Daily Unique Users / Unique Students Viewed

64 | This graph shows the daily number of unique users and unique students viewed 65 |
66 | 67 |
68 | 71 |
72 | 73 |
74 | 75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | ### General (Databases, Django, Authentication, Caching) 2 | 3 | # Default database values 4 | DJANGO_DB_ENGINE=django.db.backends.mysql 5 | DJANGO_DB_NAME=student_explorer 6 | DJANGO_DB_USER=student_explorer 7 | DJANGO_DB_PASSWORD=student_explorer 8 | DJANGO_DB_HOST=student_explorer_mysql 9 | DJANGO_DB_PORT=3306 10 | 11 | DJANGO_SEUMICH_DB_ENGINE=django.db.backends.mysql 12 | DJANGO_SEUMICH_DB_NAME=student_explorer 13 | DJANGO_SEUMICH_DB_USER=student_explorer 14 | DJANGO_SEUMICH_DB_PASSWORD=student_explorer 15 | DJANGO_SEUMICH_DB_HOST=student_explorer_mysql 16 | DJANGO_SEUMICH_DB_PORT=3306 17 | 18 | # Prefix to idenity Student Explorer emails 19 | EMAIL_SUBJECT_PREFIX=Student_Explorer 20 | MYSQL_ROOT_PASSWORD=root 21 | DJANGO_DEBUG=true 22 | 23 | # Debug level can conflict with Django Debug Toolbar 24 | DJANGO_LOGGING_LEVEL=INFO 25 | 26 | # department affiliation string from MCommunity settings 27 | DEPT_AFFILIATION 28 | 29 | # Uncomment these values and define the location and options of the cache 30 | # if you want to enable Redis (or other) caching 31 | 32 | # Django supports many different caches please see the documentation 33 | # https://docs.djangoproject.com/en/1.11/topics/cache/ 34 | # 35 | # Redis and django_mysql are included in requirements.txt 36 | 37 | # Default value in seconds for how long to cache (Default 600) 38 | # CACHE_TTL=600 39 | 40 | # Default Key Prefix (as part of the key/value pair) 41 | # CACHE_KEY_PREFIX=student_explorer 42 | 43 | # Redis, Database LocMem and others are supported 44 | # If you don't want any cache set this to 45 | # django.core.cache.backends.dummy.DummyCache 46 | 47 | # Possible Options (Others in django.core may be supported) 48 | # Only uncomment one CACHE_BACKEND and one CACHE_LOCATION 49 | # ** DummyCache - doesn't do anything 50 | # CACHE_BACKEND=django.core.cache.backends.dummy.DummyCache 51 | # CACHE_LOCATION= 52 | 53 | # ** Redis, the CACHE_LOCATION is the server and port 54 | # CACHE_BACKEND=django_redis.cache.RedisCache 55 | # CACHE_LOCATION=redis://student_explorer_redis:6379/1 56 | 57 | # ** Database CACHE_LOCATION is a table name in the 'default' database (MySQL) 58 | # This table is created automatically and could be anything. 59 | # CACHE_BACKEND=django.core.cache.backends.db.DatabaseCache 60 | # CACHE_LOCATION=se_view_cache 61 | 62 | # ** MySQL CACHE_LOCATION is created by a migration and has to be this table 63 | # This library supports CACHE_OPTIONS, see this for more info: 64 | # https://django-mysql.readthedocs.io/en/latest/cache.html 65 | # CACHE_BACKEND=django_mysql.cache.MySQLCache 66 | # CACHE_LOCATION=django_mysql_cache 67 | 68 | # These are additional options (only required for Redis) 69 | # JSON formatted options to pass (optional) 70 | # For Redis you might have 71 | # CACHE_OPTIONS={"CLIENT_CLASS": "django_redis.client.DefaultClient"} 72 | 73 | # For django_mysql you might want these options. 74 | # To turn compression off set COMPRESS_MIN_LENGTH to 0 75 | # CACHE_OPTIONS={"COMPRESS_MIN_LENGTH": 5000, "COMPRESS_LEVEL": 6} 76 | 77 | # Google Analytics 4 ID, if left blank will not use GA4 tracking 78 | # GA4_CONFIG_ID=G-XXXXXXXXX 79 | -------------------------------------------------------------------------------- /student_explorer/templates/student_explorer/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'student_explorer/index.html' %} 2 | 3 | {% block content %} 4 |
5 |

About Student Explorer

6 |

Student Explorer is an early warning system that helps academic advisors identify at-risk students 7 | based on current term Learning Management System data. It presents actionable intelligence to academic 8 | advisors using visualizations of data about students' current term grades, activity, and assignments. 9 | By helping them identify students at risk of academic jeopardy, the tool enables advisors to create timely 10 | interventions directing students toward resources that may facilitate behavioral change and academic growth 11 | and success. For more information about how Student Explorer works and how to use it, see 12 | Student Explorer 13 | Resources & Support. 14 |

15 | 16 |

Student Explorer Access

17 |

Student Explorer is available to professional staff/faculty advisors at the U-M Ann Arbor campus upon 18 | request for access. It includes information about all students on the Ann Arbor campus who use Canvas, 19 | including graduate students (note: some programs do not use Canvas and those students will not be 20 | visible in Student Explorer). Learn 21 | how to gain access to Student Explorer and how access is governed. 22 |

23 | 24 |

Student Explorer History

25 |

Student Explorer started in 2011 as a University of Michigan USE Lab research project led by Professor 26 | Stephanie Teasley and Dr. Steven Lonn in partnership with M-STEM Academies. LSA Technology Services, 27 | the Center for Academic Innovation, and ITS Teaching & Learning have contributed to the design, development, 28 | and management of the tool. 29 |

30 | 31 |

Contact the Student Explorer Team

32 |

ITS Teaching & Learning manages Student Explorer; Get in touch by 33 | emailing student-explorer-help@umich.edu! 34 |

35 | 36 |
37 | 38 |

Technical Information

39 |

The GitHub repository for Student Explorer 40 | is public. View release notes 41 | for Student Explorer. 42 |

43 | 44 |

Build Name: 45 | {{ build_name }} 46 |

47 |

Build Commit: 48 | {{ build_commit }} 49 |

50 |

Build Reference: 51 | {{ build_reference }} 52 |

53 |
54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /management/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | 4 | from management.models import Cohort 5 | 6 | import csv 7 | import os 8 | import re 9 | 10 | 11 | class UserCreateForm(forms.ModelForm): 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(UserCreateForm, self).__init__(*args, **kwargs) 15 | for field in iter(self.fields): 16 | self.fields[field].widget.attrs.update({ 17 | 'class': 'form-control' 18 | }) 19 | 20 | def save(self, commit=True): 21 | user = super(UserCreateForm, self).save(commit=False) 22 | default_password = User.objects.make_random_password() 23 | user.set_password(default_password) 24 | if commit: 25 | user.save() 26 | return user 27 | 28 | class Meta: 29 | model = User 30 | fields = ('username',) 31 | 32 | 33 | class CohortForm(forms.ModelForm): 34 | members = forms.CharField(widget=forms.Textarea, 35 | help_text=('Add two columns: student uniqname ' 36 | 'and mentor uniqname. ' 37 | 'Separate the columns with tabs, ' 38 | 'spaces or commas.'), 39 | required=False) 40 | excel_file = forms.FileField(label='Select an Excel file to Import:', 41 | help_text=('Add two columns: student ' 42 | 'uniqname and mentor uniqname. ' 43 | 'Separate the columns with tabs, ' 44 | 'spaces or commas.'), 45 | required=False) 46 | 47 | def __init__(self, *args, **kwargs): 48 | super(CohortForm, self).__init__(*args, **kwargs) 49 | for field in iter(self.fields): 50 | self.fields[field].widget.attrs.update({ 51 | 'class': 'form-control' 52 | }) 53 | 54 | def clean_members(self): 55 | data = self.cleaned_data['members'] 56 | if data: 57 | sniffer = csv.Sniffer() 58 | members = data.split('\r\n') 59 | dialect = sniffer.sniff(members[0]) 60 | delimiter = dialect.delimiter 61 | for line_number, member in enumerate(members): 62 | record = member.split(delimiter) 63 | if len(record) < 2: 64 | raise forms.ValidationError(f"Inconsistent Delimiter at Line {line_number + 1}") 65 | return data 66 | 67 | def clean_excel_file(self): 68 | data = self.cleaned_data['excel_file'] 69 | if data: 70 | ext = os.path.splitext(data.name)[1] 71 | valid_extensions = ['.xlsx', '.xls'] 72 | if not ext.lower() in valid_extensions: 73 | raise forms.ValidationError("Unsupported file extension") 74 | return data 75 | 76 | def clean(self): 77 | cleaned_data = super(CohortForm, self).clean() 78 | 79 | # verify the code input 80 | code = cleaned_data.get('code') 81 | if (not bool(re.match('^[-\w\s]+$', code))): 82 | msg = ("Please enter only alphanumeric, space, or dash characters for cohort code field") 83 | self.add_error('code', msg) 84 | 85 | members = cleaned_data.get('members') 86 | excel_file = cleaned_data.get('excel_file') 87 | 88 | if (not members and not excel_file) or (members and excel_file): 89 | msg = ("Please enter valid records in the textarea OR " 90 | "upload a valid excel file") 91 | self.add_error('members', msg) 92 | self.add_error('excel_file', msg) 93 | 94 | return cleaned_data 95 | 96 | class Meta: 97 | model = Cohort 98 | fields = ('code', 'description', 'group',) 99 | -------------------------------------------------------------------------------- /management/templates/management/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'management/base.html' %} 2 | 3 | {% block management %} 4 | 5 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% for user in object_list %} 29 | 30 | 37 | 38 | 39 | 83 | 84 | {% empty %} 85 | 86 | 87 | 88 | {% endfor %} 89 | 90 |
6 | {% if show == "active" %} 7 | Active Users 8 | 9 | Show all users 10 | 11 | {% endif %} 12 | {% if show == "all" %} 13 | All Users 14 | 15 | Show only active users 16 | 17 | {% endif %} 18 |
UsernameFirst nameLast nameAction
31 | {% if user.is_active %} 32 | {{ user.username }} 33 | {% else %} 34 | {{ user.username }} 35 | {% endif %} 36 | {{ user.first_name }}{{ user.last_name }} 40 | Lookup 41 | | 42 | 43 | {% if user.is_active %} 44 | Deactivate 45 | {% else %} 46 | Activate 47 | {% endif %} 48 | 49 | 50 | 51 | 81 | 82 |
None Found
91 | 92 | 93 | Add User 94 | 95 | {% include 'management/pagination.html' %} 96 | {% endblock %} 97 | -------------------------------------------------------------------------------- /student_explorer/fixtures/dev_users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "username": "admin", 5 | "first_name": "", 6 | "last_name": "", 7 | "is_active": true, 8 | "is_superuser": true, 9 | "is_staff": true, 10 | "last_login": "2015-10-15T13:50:50Z", 11 | "groups": [], 12 | "user_permissions": [], 13 | "password": "pbkdf2_sha256$20000$ht9UebIr86Ro$MSZWRZ8PZg7uzVtP6J5L6EL5wckcwzgrhHlXrDC50vs=", 14 | "email": "", 15 | "date_joined": "2015-10-15T13:13:28Z" 16 | }, 17 | "model": "auth.user", 18 | "pk": 1 19 | }, 20 | { 21 | "fields": { 22 | "username": "zander", 23 | "first_name": "Zander", 24 | "last_name": "Agrippa", 25 | "is_active": true, 26 | "is_superuser": false, 27 | "is_staff": false, 28 | "last_login": "2015-10-15T19:32:09Z", 29 | "groups": [], 30 | "user_permissions": [], 31 | "password": "pbkdf2_sha256$20000$gI3Uo8BIwQ5r$MCMJO5VqFvjpD+J0mBGEe6/2jqr3gzhgMTIPdT+Nzso=", 32 | "email": "", 33 | "date_joined": "2015-10-15T13:56:14Z" 34 | }, 35 | "model": "auth.user", 36 | "pk": 6 37 | }, 38 | { 39 | "fields": { 40 | "username": "carla", 41 | "first_name": "Calra", 42 | "last_name": "Beumer", 43 | "is_active": true, 44 | "is_superuser": false, 45 | "is_staff": false, 46 | "last_login": "2015-10-15T14:44:40Z", 47 | "groups": [], 48 | "user_permissions": [], 49 | "password": "pbkdf2_sha256$20000$a8hLjNk8D3o3$g+PQIHHOu8jJFPjqsITqYhOTqCugZaX/bwK80ycpYLU=", 50 | "email": "", 51 | "date_joined": "2015-10-15T13:56:33Z" 52 | }, 53 | "model": "auth.user", 54 | "pk": 7 55 | }, 56 | { 57 | "fields": { 58 | "username": "burl", 59 | "first_name": "Burl", 60 | "last_name": "Chandler", 61 | "is_active": true, 62 | "is_superuser": false, 63 | "is_staff": false, 64 | "last_login": "2015-11-18T19:16:37Z", 65 | "groups": [], 66 | "user_permissions": [], 67 | "password": "pbkdf2_sha256$20000$93rkHn7zyOIm$q02j77FK+u012Tp4afivmt7vcpJ+3f+e6as7xX9gCPY=", 68 | "email": "", 69 | "date_joined": "2015-10-15T13:56:47Z" 70 | }, 71 | "model": "auth.user", 72 | "pk": 8 73 | }, 74 | { 75 | "fields": { 76 | "username": "lavera", 77 | "first_name": "Lavera", 78 | "last_name": "Rumore", 79 | "is_active": true, 80 | "is_superuser": false, 81 | "is_staff": false, 82 | "last_login": "2015-10-15T16:50:56Z", 83 | "groups": [], 84 | "user_permissions": [], 85 | "password": "pbkdf2_sha256$20000$sPYRQ4g8vha4$xKXwFWayBg0MewmGcJC4ZhRojlXTsrHC97/F7k1FJ7c=", 86 | "email": "", 87 | "date_joined": "2015-10-15T13:57:33Z" 88 | }, 89 | "model": "auth.user", 90 | "pk": 9 91 | }, 92 | { 93 | "fields": { 94 | "username": "will", 95 | "first_name": "Will", 96 | "last_name": "Smith", 97 | "is_active": true, 98 | "is_superuser": false, 99 | "is_staff": false, 100 | "last_login": null, 101 | "groups": [], 102 | "user_permissions": [], 103 | "password": "pbkdf2_sha256$20000$C4Mqxf0vCc5l$cK0XXzK588MRH0cks/oDdABJyfH+qqhV4gW2ZRvPJFY=", 104 | "email": "", 105 | "date_joined": "2015-10-15T13:57:45Z" 106 | }, 107 | "model": "auth.user", 108 | "pk": 10 109 | }, 110 | { 111 | "fields": { 112 | "username": "mollie", 113 | "first_name": "Mollie", 114 | "last_name": "Whistler", 115 | "is_active": true, 116 | "is_superuser": false, 117 | "is_staff": false, 118 | "last_login": null, 119 | "groups": [], 120 | "user_permissions": [], 121 | "password": "pbkdf2_sha256$20000$S7SlqqqQWj89$Z0xmB1WjDMhw1vdS6ub+p9JNR77O/TjvRG5tAd5AHXs=", 122 | "email": "", 123 | "date_joined": "2015-10-15T13:57:59Z" 124 | }, 125 | "model": "auth.user", 126 | "pk": 11 127 | }, 128 | { 129 | "fields": { 130 | "username": "smrech", 131 | "first_name": "Sara", 132 | "last_name": "RechnitzerProfile", 133 | "is_active": true, 134 | "is_superuser": false, 135 | "is_staff": false, 136 | "last_login": "2015-11-18T19:16:37Z", 137 | "groups": [], 138 | "user_permissions": [], 139 | "password": "pbkdf2_sha256$20000$93rkHn7zyOIm$q02j77FK+u012Tp4afivmt7vcpJ+3f+e6as7xX9gCPY=", 140 | "email": "", 141 | "date_joined": "2015-10-15T13:56:47Z" 142 | }, 143 | "model": "auth.user", 144 | "pk": 12 145 | } 146 | ] 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Student Explorer 2 | 3 | ## Overview 4 | 5 | Student Explorer is an early warning system that helps academic advisors at the University of Michigan-Ann Arbor identify at-risk students based on current term Learning Management System data. It started in 2011 as a research project at the University of Michigan USE Lab; LSA Technology Services, Academic Innovation, and ITS Teaching & Learning have contributed to the design, development, and management of the tool. Student Explorer is in use as an enterprise production application at U-M and continues to be developed by ITS Teaching & Learning. The code is open source and is licensed under Apache 2.0. The application primarily relies on Django for the backend and Bootstrap and jQuery for the frontend. 6 | 7 | - [About the Student Explorer Application](https://studentexplorer.ai.umich.edu/about) 8 | - [Student Explorer Documentation](https://documentation.its.umich.edu/student-explorer-general) 9 | - [Student Explorer Release Notes](https://github.com/tl-its-umich-edu/student_explorer/releases) 10 | 11 | To contact the Student Explorer team, please email student-explorer-help@umich.edu. 12 | 13 | ## Data Sources 14 | 15 | Student Explorer uses two separate data sources in production: 16 | 17 | 1. A MySQL database, which contains administrative and management tables typical for Django databases, including django_admin_log (log of changes made in Django admin), tracking_event (log of events tracked in the application such as page views, logins, and redirects), and auth_user (user accounts). This database also maintains the cohort relationships and is being used to cache page templates to improve performance. 18 | 19 | 2. U-M Data Warehouse Oracle database edwprod.world, which contains the data served up in the application, including basic student and course data, grades, assignments, etc. For full information on the tables in the U-M Data Warehouse that are used by Student Explorer, see [Teaching & Learning Student Explorer dataset information.](https://its.umich.edu/enterprise/administrative-systems/data-warehouse/data-areas/teaching-learning#student-explorer) 20 | 21 | Note that when you follow the instructions below to start up a local version of the application, both data sources will be created in the same MySQL database and populated with fake data. This data will help demonstrate the tool's functionality. 22 | 23 | ## Development Environment 24 | 25 | ### Application Setup 26 | 27 | To follow these instructions, you will need to have [Docker](https://www.docker.com/) installed. For those new to the 28 | technology, the [documentation](https://docs.docker.com/) includes a detailed introduction. 29 | 30 | 1. Clone and navigate into the repository. 31 | 32 | ``` 33 | git clone https://github.com/tl-its-umich-edu/student_explorer.git # HTTPS 34 | git clone git@github.com:tl-its-umich-edu/student_explorer.git # SSH 35 | 36 | cd student_explorer 37 | ``` 38 | 39 | 2. Create a `.env` using `.env.sample` as a template. 40 | 41 | ``` 42 | mv .env.sample .env 43 | ``` 44 | 45 | 3. Build and bring up the development server using `docker-compose`. 46 | 47 | ``` 48 | docker-compose build 49 | docker-compose up 50 | ``` 51 | 52 | Use `^C` and `docker-compose down` to bring the application down. 53 | 54 | 4. Browse the application on localhost. 55 | 56 | - Navigate to [http://localhost:2082/](http://localhost:2082/). 57 | - Log in as `admin` or an advisor (e.g. `burl`). All passwords are the same as the user's username. 58 | 59 | Not all pages will have complete data. The pages for 60 | [Grace Devilbiss](http://localhost:2082/students/grace/) provide a comprehensive example of how the tool 61 | presents and visualizes student data. 62 | 63 | ### Running the Tests 64 | 65 | When working on the application, you should periodically run and update the unit tests. To run them, use 66 | the following command while the development server is up. 67 | 68 | ``` 69 | docker exec -it student_explorer /bin/bash -c "echo yes | python manage.py test" 70 | ``` 71 | 72 | You can also enter the running Docker container — and then run your own commands — by using the first part 73 | of the above command, `docker exec -it student_explorer /bin/bash`. 74 | 75 | ### Django Debug Toolbar 76 | 77 | The application is currently configured to use the 78 | [Django Debug Toolbar](https://django-debug-toolbar.readthedocs.io/en/latest/) to assist with development. The toolbar 79 | will only appear if `DJANGO_DEBUG` is set to `true` in the `.env` and the user is logged in as a superuser. 80 | 81 | ### Note on Settings 82 | 83 | By default, Django's `manage.py` process looks for the `student_explorer.local.settings_override module`. This file is 84 | created automatically when using `docker-compose` based on instructions in the `Dockerfile`. In addition, `wsgi.py` 85 | (which is used by `start.sh`) looks by default for the `student_explorer.settings` module. This file is versioned as 86 | part of the repository. This behavior can be changed for both `manage.py` and `wsgi.py` by setting the 87 | `DJANGO_SETTINGS_MODULE` environment variable. 88 | -------------------------------------------------------------------------------- /seumich/templates/seumich/student_list_partial.html: -------------------------------------------------------------------------------- 1 | {% load filters %} 2 | {% load static from static %} 3 | 4 | 5 | 6 |

{{ studentListHeader }}

7 |
8 |

Students

9 | 10 |
11 | 12 | 13 | 14 | 17 | 20 | 23 | 26 | 29 | 32 | 33 | 34 | 35 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 55 | 56 | {% include 'seumich/ts_pager.html' with colspan="6" %} 57 | 58 | 59 | 60 | {% for student in students %} 61 | 62 | 65 | 68 | 71 | 72 | 99 | 104 | 105 | {% endfor %} 106 | 107 |
15 | First Name 16 | 18 | Last Name 19 | 21 | Uniqname 22 | 24 | Student ID 25 | 27 | Status 28 | 30 | Cohorts 31 |
37 | First Name 38 | 40 | Last Name 41 | 43 | Uniqname 44 | 46 | Student ID 47 | 49 | Status 50 | 52 | Cohorts 53 |
63 | {{ student.first_name }} 64 | 66 | {{ student.last_name }} 67 | 69 | {{ student.username }} 70 | {{ student.univ_id }} 73 | {% for element in student.studentclasssitestatus_set.all %} 74 | {% with class_site=element.class_site %} 75 | 76 | {% with status=element.status %} 77 | {% if status.description == 'Green' %} 78 | 79 | Green encourage status icon 80 | 81 | {% elif status.description == 'Yellow' %} 82 | 83 | Yellow explore status icon 84 | 85 | {% elif status.description == 'Red' %} 86 | 87 | Red engage status icon 88 | 89 | {% elif status.description == 'Not Applicable' %} 90 | 91 | no status available for this course icon 92 | 93 | {% endif %} 94 | {% endwith %} 95 | 96 | {% endwith %} 97 | {% endfor %} 98 | 100 | {% for cohort in student.cohorts.all %} 101 |
{{ cohort }}
102 | {% endfor %} 103 |
108 | 109 |
110 | -------------------------------------------------------------------------------- /usage/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import View, TemplateView 2 | from django.db import models 3 | from django.db.models import Count, Func 4 | from django.contrib.admin.views.decorators import staff_member_required 5 | from django.utils.decorators import method_decorator 6 | from tracking.models import Event 7 | from django.utils import timezone 8 | from django.conf import settings 9 | from django.http import HttpResponse 10 | 11 | 12 | import datetime 13 | import csv 14 | 15 | 16 | class ExtractUnixTimestamp(Func): 17 | function = 'UNIX_TIMESTAMP' 18 | template = '%(function)s(%(expressions)s)' 19 | output_field = models.IntegerField() 20 | 21 | 22 | class ExtractDate(Func): 23 | function = 'DATE' 24 | template = '%(function)s(%(expressions)s)' 25 | output_field = models.DateField() 26 | 27 | 28 | class ExtractSubString(Func): 29 | function = 'SUBSTRING' 30 | template = '%(function)s(%(expressions)s, 11)' 31 | output_field = models.CharField() 32 | 33 | 34 | class ExtractStudent(Func): 35 | function = 'SUBSTRING_INDEX' 36 | template = '%(function)s(%(expressions)s, "/", 1)' 37 | output_field = models.CharField() 38 | 39 | 40 | class StaffMemberRequiredMixin(object): 41 | 42 | @method_decorator(staff_member_required) 43 | def dispatch(self, request, *args, **kwargs): 44 | return super(StaffMemberRequiredMixin, self).dispatch(request, 45 | *args, 46 | **kwargs) 47 | 48 | 49 | class PastDataMixin(object): 50 | 51 | def __init__(self): 52 | self.current = None 53 | self.last = None 54 | 55 | def next_weekday(self, d, weekday): 56 | days_ahead = weekday - d.weekday() 57 | if days_ahead <= 0: 58 | days_ahead += 7 59 | return d + datetime.timedelta(days_ahead) 60 | 61 | def get_past_acad_year(self): 62 | 63 | sept_current = datetime.datetime(timezone.now().year, 64 | month=9, 65 | day=1) 66 | sept_current = self.next_weekday(sept_current, 0) 67 | sept_last = datetime.datetime(timezone.now().year - 1, 68 | month=9, 69 | day=1) 70 | sept_last = self.next_weekday(sept_last, 0) 71 | current_acad_year = timezone.make_aware( 72 | sept_current, 73 | timezone.get_current_timezone()) 74 | last_acad_year = timezone.make_aware( 75 | sept_last, 76 | timezone.get_current_timezone()) 77 | 78 | self.current = current_acad_year 79 | self.last = last_acad_year 80 | 81 | def get_past_users(self): 82 | self.get_past_acad_year() 83 | pastUsers = (Event.objects 84 | .filter( 85 | timestamp__lte=self.current, 86 | timestamp__gte=self.last 87 | ) 88 | .values_list('user__username', flat=True) 89 | .distinct() 90 | .order_by()) 91 | return pastUsers 92 | 93 | def get_past_students(self): 94 | self.get_past_acad_year() 95 | pastStudents = (Event.objects 96 | .filter( 97 | note__regex=r'^/students/[a-zA-Z]+/', 98 | timestamp__lte=self.current, 99 | timestamp__gte=self.last 100 | ) 101 | .annotate(student=ExtractStudent( 102 | ExtractSubString('note'))) 103 | .values_list('student', flat=True) 104 | .distinct() 105 | .order_by()) 106 | return pastStudents 107 | 108 | 109 | class UsageView(StaffMemberRequiredMixin, PastDataMixin, TemplateView): 110 | template_name = 'usage.html' 111 | 112 | def get_daily_user_data(self, startdate): 113 | dailydata = (Event.objects 114 | .filter( 115 | timestamp__gte=startdate 116 | ) 117 | .annotate( 118 | date=ExtractUnixTimestamp( 119 | ExtractDate('timestamp'))) 120 | .values('date') 121 | .annotate(count=Count('user', distinct=True)) 122 | .order_by()) 123 | return dailydata 124 | 125 | def get_daily_student_data(self, startdate): 126 | dailydata = (Event.objects 127 | .filter( 128 | note__regex=r'^/students/[a-zA-Z]+/', 129 | timestamp__gte=startdate 130 | ) 131 | .annotate( 132 | date=ExtractUnixTimestamp( 133 | ExtractDate('timestamp'))) 134 | .values('date') 135 | .annotate( 136 | count=Count( 137 | ExtractStudent( 138 | ExtractSubString('note')), 139 | distinct=True)) 140 | .order_by()) 141 | return dailydata 142 | 143 | def get_context_data(self, **kwargs): 144 | context = super(UsageView, self).get_context_data(**kwargs) 145 | usage_past_weeks = settings.USAGE_PAST_WEEKS 146 | startdate = timezone.now() - datetime.timedelta(weeks=usage_past_weeks) 147 | # get next monday and set time to zero 148 | startdate = startdate + datetime.timedelta( 149 | days=(7 - startdate.weekday()) 150 | ) 151 | startdate = startdate.replace(hour=0, minute=0, second=0) 152 | 153 | dailyuserdata = {'key': 'Unique Users Count', 'values': list( 154 | self.get_daily_user_data(startdate)), 'color': '#7777ff', 155 | 'area': 'true'} 156 | dailystudentdata = {'key': 'Unique Students Viewed', 'values': list( 157 | self.get_daily_student_data(startdate)), 'color': '#ff7f0e'} 158 | 159 | dailyData = [] 160 | dailyData.append(dailystudentdata) 161 | dailyData.append(dailyuserdata) 162 | 163 | context['dailyData'] = dailyData 164 | context['usersCount'] = self.get_past_users().count() 165 | context['studentsCount'] = self.get_past_students().count() 166 | return context 167 | 168 | 169 | class DownloadCsvView(View, PastDataMixin): 170 | 171 | def render_to_csv(self): 172 | response = HttpResponse(content_type='text/csv') 173 | response['Content-Disposition'] = ('attachment; ' 174 | 'filename="usernamelist.csv"') 175 | 176 | writer = csv.writer(response) 177 | writer.writerow(['UserName']) 178 | for user in self.get_past_users(): 179 | writer.writerow([user]) 180 | return response 181 | 182 | def get(self, request, *args, **kwargs): 183 | return self.render_to_csv() 184 | -------------------------------------------------------------------------------- /management/fixtures/student_mentor_mappings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "student_id": "hobbyistone", 4 | "code": "98FallWhistling", 5 | "mentor_id": "hobbymentorone" 6 | }, 7 | { 8 | "student_id": "hobbyisttwo", 9 | "code": "98FallSleeping", 10 | "mentor_id": "hobbymentorone" 11 | }, 12 | { 13 | "student_id": "hobbyistthree", 14 | "code": "98FallRunning", 15 | "mentor_id": "hobbymentorone" 16 | }, 17 | { 18 | "student_id": "hobbyistfour", 19 | "code": "98FallDreaming", 20 | "mentor_id": "hobbymentorone" 21 | }, 22 | { 23 | "student_id": "hobbyistfive", 24 | "code": "98FallEating", 25 | "mentor_id": "hobbymentorone" 26 | }, 27 | { 28 | "student_id": "hobbyistsix", 29 | "code": "98FallJumping", 30 | "mentor_id": "hobbymentorone" 31 | }, 32 | { 33 | "student_id": "hobbyistseven", 34 | "code": "98FallStretching", 35 | "mentor_id": "hobbymentorone" 36 | }, 37 | { 38 | "student_id": "hobbyisteight", 39 | "code": "98FallWaltzing", 40 | "mentor_id": "hobbymentorone" 41 | }, 42 | { 43 | "student_id": "hobbyistnine", 44 | "code": "98FallSkateboarding", 45 | "mentor_id": "hobbymentorone" 46 | }, 47 | { 48 | "student_id": "hobbyistten", 49 | "code": "98FallJuggling", 50 | "mentor_id": "hobbymentorone" 51 | }, 52 | { 53 | "student_id": "hobbyisteleven", 54 | "code": "98FallSkipping", 55 | "mentor_id": "hobbymentortwo" 56 | }, 57 | { 58 | "student_id": "hobbyisttwelve", 59 | "code": "98FallThinking", 60 | "mentor_id": "hobbymentortwo" 61 | }, 62 | { 63 | "student_id": "hobbyistthirteen", 64 | "code": "98FallCooking", 65 | "mentor_id": "hobbymentortwo" 66 | }, 67 | { 68 | "student_id": "hobbyistfourteen", 69 | "code": "98FallActivisting", 70 | "mentor_id": "hobbymentortwo" 71 | }, 72 | { 73 | "student_id": "hobbyistfifteen", 74 | "code": "98FallReading", 75 | "mentor_id": "hobbymentortwo" 76 | }, 77 | { 78 | "student_id": "hobbyistsixteen", 79 | "code": "98FallGaming", 80 | "mentor_id": "hobbymentortwo" 81 | }, 82 | { 83 | "student_id": "hobbyistseventeen", 84 | "code": "98FallHaircutting", 85 | "mentor_id": "hobbymentortwo" 86 | }, 87 | { 88 | "student_id": "hobbyisteightteen", 89 | "code": "98FallPuzzling", 90 | "mentor_id": "hobbymentortwo" 91 | }, 92 | { 93 | "student_id": "hobbyistnineteen", 94 | "code": "98FallKnitting", 95 | "mentor_id": "hobbymentortwo" 96 | }, 97 | { 98 | "student_id": "hobbyisttwenty", 99 | "code": "98FallWoodworking", 100 | "mentor_id": "hobbymentortwo" 101 | }, 102 | { 103 | "student_id": "hobbyisttwentyone", 104 | "code": "98FallCollaging", 105 | "mentor_id": "hobbymentorthree" 106 | }, 107 | { 108 | "student_id": "hobbyisttwentytwo", 109 | "code": "98FallWhispering", 110 | "mentor_id": "hobbymentorthree" 111 | }, 112 | { 113 | "student_id": "hobbyisttwentythree", 114 | "code": "98FallStyling", 115 | "mentor_id": "hobbymentorthree" 116 | }, 117 | { 118 | "student_id": "hobbyisttwentyfour", 119 | "code": "98FallTriviaing", 120 | "mentor_id": "hobbymentorthree" 121 | }, 122 | { 123 | "student_id": "hobbyistwentyfive", 124 | "code": "98FallSocializing", 125 | "mentor_id": "hobbymentorthree" 126 | }, 127 | { 128 | "student_id": "hobbyistone", 129 | "code": "99WinterWhistling", 130 | "mentor_id": "hobbymentorone" 131 | }, 132 | { 133 | "student_id": "hobbyisttwo", 134 | "code": "99WinterSleeping", 135 | "mentor_id": "hobbymentorone" 136 | }, 137 | { 138 | "student_id": "hobbyistthree", 139 | "code": "99WinterRunning", 140 | "mentor_id": "hobbymentorone" 141 | }, 142 | { 143 | "student_id": "hobbyistfour", 144 | "code": "99WinterDreaming", 145 | "mentor_id": "hobbymentorone" 146 | }, 147 | { 148 | "student_id": "hobbyistfive", 149 | "code": "99WinterEating", 150 | "mentor_id": "hobbymentorone" 151 | }, 152 | { 153 | "student_id": "hobbyistsix", 154 | "code": "99WinterJumping", 155 | "mentor_id": "hobbymentorone" 156 | }, 157 | { 158 | "student_id": "hobbyistseven", 159 | "code": "99WinterStretching", 160 | "mentor_id": "hobbymentorone" 161 | }, 162 | { 163 | "student_id": "hobbyisteight", 164 | "code": "99WinterWaltzing", 165 | "mentor_id": "hobbymentorone" 166 | }, 167 | { 168 | "student_id": "hobbyistnine", 169 | "code": "99WinterSkateboarding", 170 | "mentor_id": "hobbymentorone" 171 | }, 172 | { 173 | "student_id": "hobbyistten", 174 | "code": "99WinterJuggling", 175 | "mentor_id": "hobbymentorone" 176 | }, 177 | { 178 | "student_id": "hobbyisteleven", 179 | "code": "99WinterSkipping", 180 | "mentor_id": "hobbymentortwo" 181 | }, 182 | { 183 | "student_id": "hobbyisttwelve", 184 | "code": "99WinterThinking", 185 | "mentor_id": "hobbymentortwo" 186 | }, 187 | { 188 | "student_id": "hobbyistthirteen", 189 | "code": "99WinterCooking", 190 | "mentor_id": "hobbymentortwo" 191 | }, 192 | { 193 | "student_id": "hobbyistfourteen", 194 | "code": "99WinterActivisting", 195 | "mentor_id": "hobbymentortwo" 196 | }, 197 | { 198 | "student_id": "hobbyistfifteen", 199 | "code": "99WinterReading", 200 | "mentor_id": "hobbymentortwo" 201 | }, 202 | { 203 | "student_id": "hobbyistsixteen", 204 | "code": "99WinterGaming", 205 | "mentor_id": "hobbymentortwo" 206 | }, 207 | { 208 | "student_id": "hobbyistseventeen", 209 | "code": "99WinterHaircutting", 210 | "mentor_id": "hobbymentortwo" 211 | }, 212 | { 213 | "student_id": "hobbyisteightteen", 214 | "code": "99WinterPuzzling", 215 | "mentor_id": "hobbymentortwo" 216 | }, 217 | { 218 | "student_id": "hobbyistnineteen", 219 | "code": "99WinterKnitting", 220 | "mentor_id": "hobbymentortwo" 221 | }, 222 | { 223 | "student_id": "hobbyisttwenty", 224 | "code": "99WinterWoodworking", 225 | "mentor_id": "hobbymentortwo" 226 | }, 227 | { 228 | "student_id": "hobbyisttwentyone", 229 | "code": "99WinterCollaging", 230 | "mentor_id": "hobbymentorthree" 231 | }, 232 | { 233 | "student_id": "hobbyisttwentytwo", 234 | "code": "99WinterWhispering", 235 | "mentor_id": "hobbymentorthree" 236 | }, 237 | { 238 | "student_id": "hobbyisttwentythree", 239 | "code": "99WinterStyling", 240 | "mentor_id": "hobbymentorthree" 241 | }, 242 | { 243 | "student_id": "hobbyisttwentyfour", 244 | "code": "99WinterTriviaing", 245 | "mentor_id": "hobbymentorthree" 246 | }, 247 | { 248 | "student_id": "hobbyistwentyfive", 249 | "code": "99WinterSocializing", 250 | "mentor_id": "hobbymentorthree" 251 | }, 252 | { 253 | "student_id": "hobbyistwentysix", 254 | "code": "99WinterJoking", 255 | "mentor_id": "hobbymentorthree" 256 | } 257 | ] -------------------------------------------------------------------------------- /management/templates/management/cohort_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'management/base.html' %} 2 | {% load static %} 3 | {% block management %} 4 | 5 | 6 | 7 | 8 | Add Cohort 9 | 10 | 11 | 12 | Download TLA_Cohort_USELAB.dat 13 | 14 | 15 | 16 | Download TLA_StudentCohortMentor_USELAB.dat 17 | 18 |
19 | 20 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {% include 'seumich/ts_pager.html' with colspan="6" %} 51 | 52 | 53 | 54 | {% for cohort in object_list %} 55 | 56 | 65 | 66 | 67 | 141 | 142 | {% empty %} 143 | 144 | 145 | 146 | {% endfor %} 147 | 148 |
21 | {% if show == "active" %} 22 | Active Cohorts 23 | 24 | Show all cohorts 25 | 26 | {% endif %} 27 | {% if show == "all" %} 28 | All Cohorts 29 | 30 | Show only active cohorts 31 | 32 | {% endif %} 33 |
CodeDescriptionGroupAction
CodeDescriptionGroupAction
57 | 58 | {% if cohort.active %} 59 | {{ cohort.code }} 60 | {% else %} 61 | {{ cohort.code }} 62 | {% endif %} 63 | 64 | {{ cohort.description }}{{ cohort.group }} 68 | 69 | 70 | {% if cohort.active %} 71 | Deactivate 72 | {% else %} 73 | Activate 74 | {% endif %} 75 | 76 | | 77 | 78 | Delete 79 | 80 | 81 | 82 | 112 | 113 | 139 | 140 |
None Found
149 |
150 | {% endblock %} 151 | -------------------------------------------------------------------------------- /management/fixtures/cohorts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "code": "98FallWhistling", 4 | "description": "Cohort for whistlers", 5 | "group": "Hobby Action Coalition (HAC)" 6 | }, 7 | { 8 | "code": "98FallSleeping", 9 | "description": "Cohort for sleepers", 10 | "group": "Hobby Action Coalition (HAC)" 11 | }, 12 | { 13 | "code": "98FallRunning", 14 | "description": "Cohort for runners", 15 | "group": "Hobby Action Coalition (HAC)" 16 | }, 17 | { 18 | "code": "98FallDreaming", 19 | "description": "Cohort for dreamers", 20 | "group": "Hobby Action Coalition (HAC)" 21 | }, 22 | { 23 | "code": "98FallEating", 24 | "description": "Cohort for eaters", 25 | "group": "Hobby Action Coalition (HAC)" 26 | }, 27 | { 28 | "code": "98FallJumping", 29 | "description": "Cohort for jumpers", 30 | "group": "Hobby Action Coalition (HAC)" 31 | }, 32 | { 33 | "code": "98FallStretching", 34 | "description": "Cohort for stretchers", 35 | "group": "Hobby Action Coalition (HAC)" 36 | }, 37 | { 38 | "code": "98FallWaltzing", 39 | "description": "Cohort for waltzers", 40 | "group": "Hobby Action Coalition (HAC)" 41 | }, 42 | { 43 | "code": "98FallSkateboarding", 44 | "description": "Cohort for skateboarders", 45 | "group": "Hobby Action Coalition (HAC)" 46 | }, 47 | { 48 | "code": "98FallJuggling", 49 | "description": "Cohort for jugglers", 50 | "group": "Hobby Action Coalition (HAC)" 51 | }, 52 | { 53 | "code": "98FallSkipping", 54 | "description": "Cohort for people who skip", 55 | "group": "Hobby Action Coalition (HAC)" 56 | }, 57 | { 58 | "code": "98FallThinking", 59 | "description": "Cohort for thinkers", 60 | "group": "Hobby Action Coalition (HAC)" 61 | }, 62 | { 63 | "code": "98FallCooking", 64 | "description": "Cohort for cookers", 65 | "group": "Hobby Action Coalition (HAC)" 66 | }, 67 | { 68 | "code": "98FallActivisting", 69 | "description": "Cohort for activists", 70 | "group": "Hobby Action Coalition (HAC)" 71 | }, 72 | { 73 | "code": "98FallReading", 74 | "description": "Cohort for reading", 75 | "group": "Hobby Action Coalition (HAC)" 76 | }, 77 | { 78 | "code": "98FallGaming", 79 | "description": "Cohort for gamers", 80 | "group": "Hobby Action Coalition (HAC)" 81 | }, 82 | { 83 | "code": "98FallHaircutting", 84 | "description": "Cohort for haircutters", 85 | "group": "Hobby Action Coalition (HAC)" 86 | }, 87 | { 88 | "code": "98FallPuzzling", 89 | "description": "Cohort for puzzlers", 90 | "group": "Hobby Action Coalition (HAC)" 91 | }, 92 | { 93 | "code": "98FallKnitting", 94 | "description": "Cohort for knitters", 95 | "group": "Hobby Action Coalition (HAC)" 96 | }, 97 | { 98 | "code": "98FallWoodworking", 99 | "description": "Cohort for woodworkers", 100 | "group": "Hobby Action Coalition (HAC)" 101 | }, 102 | { 103 | "code": "98FallCollaging", 104 | "description": "Cohort for people who collage", 105 | "group": "Hobby Action Coalition (HAC)" 106 | }, 107 | { 108 | "code": "98FallWhispering", 109 | "description": "Cohort for whisperers", 110 | "group": "Hobby Action Coalition (HAC)" 111 | }, 112 | { 113 | "code": "98FallStyling", 114 | "description": "Cohort for who people who style", 115 | "group": "Hobby Action Coalition (HAC)" 116 | }, 117 | { 118 | "code": "98FallTriviaing", 119 | "description": "Cohort for who people who trivia", 120 | "group": "Hobby Action Coalition (HAC)" 121 | }, 122 | { 123 | "code": "98FallSocializing", 124 | "description": "Cohort for people who socialize", 125 | "group": "Hobby Action Coalition (HAC)" 126 | }, 127 | { 128 | "code": "99WinterWhistling", 129 | "description": "Cohort for whistlers", 130 | "group": "Hobby Action Coalition (HAC)" 131 | }, 132 | { 133 | "code": "99WinterSleeping", 134 | "description": "Cohort for sleepers", 135 | "group": "Hobby Action Coalition (HAC)" 136 | }, 137 | { 138 | "code": "99WinterRunning", 139 | "description": "Cohort for runners", 140 | "group": "Hobby Action Coalition (HAC)" 141 | }, 142 | { 143 | "code": "99WinterDreaming", 144 | "description": "Cohort for dreamers", 145 | "group": "Hobby Action Coalition (HAC)" 146 | }, 147 | { 148 | "code": "99WinterEating", 149 | "description": "Cohort for eaters", 150 | "group": "Hobby Action Coalition (HAC)" 151 | }, 152 | { 153 | "code": "99WinterJumping", 154 | "description": "Cohort for jumpers", 155 | "group": "Hobby Action Coalition (HAC)" 156 | }, 157 | { 158 | "code": "99WinterStretching", 159 | "description": "Cohort for stretchers", 160 | "group": "Hobby Action Coalition (HAC)" 161 | }, 162 | { 163 | "code": "99WinterWaltzing", 164 | "description": "Cohort for waltzers", 165 | "group": "Hobby Action Coalition (HAC)" 166 | }, 167 | { 168 | "code": "99WinterSkateboarding", 169 | "description": "Cohort for skateboarders", 170 | "group": "Hobby Action Coalition (HAC)" 171 | }, 172 | { 173 | "code": "99WinterJuggling", 174 | "description": "Cohort for jugglers", 175 | "group": "Hobby Action Coalition (HAC)" 176 | }, 177 | { 178 | "code": "99WinterSkipping", 179 | "description": "Cohort for people who skip", 180 | "group": "Hobby Action Coalition (HAC)" 181 | }, 182 | { 183 | "code": "99WinterThinking", 184 | "description": "Cohort for thinkers", 185 | "group": "Hobby Action Coalition (HAC)" 186 | }, 187 | { 188 | "code": "99WinterCooking", 189 | "description": "Cohort for cookers", 190 | "group": "Hobby Action Coalition (HAC)" 191 | }, 192 | { 193 | "code": "99WinterActivisting", 194 | "description": "Cohort for activists", 195 | "group": "Hobby Action Coalition (HAC)" 196 | }, 197 | { 198 | "code": "99WinterReading", 199 | "description": "Cohort for reading", 200 | "group": "Hobby Action Coalition (HAC)" 201 | }, 202 | { 203 | "code": "99WinterGaming", 204 | "description": "Cohort for gamers", 205 | "group": "Hobby Action Coalition (HAC)" 206 | }, 207 | { 208 | "code": "99WinterHaircutting", 209 | "description": "Cohort for haircutters", 210 | "group": "Hobby Action Coalition (HAC)" 211 | }, 212 | { 213 | "code": "99WinterPuzzling", 214 | "description": "Cohort for puzzlers", 215 | "group": "Hobby Action Coalition (HAC)" 216 | }, 217 | { 218 | "code": "99WinterKnitting", 219 | "description": "Cohort for knitters", 220 | "group": "Hobby Action Coalition (HAC)" 221 | }, 222 | { 223 | "code": "99WinterWoodworking", 224 | "description": "Cohort for woodworkers", 225 | "group": "Hobby Action Coalition (HAC)" 226 | }, 227 | { 228 | "code": "99WinterCollaging", 229 | "description": "Cohort for people who collage", 230 | "group": "Hobby Action Coalition (HAC)" 231 | }, 232 | { 233 | "code": "99WinterWhispering", 234 | "description": "Cohort for whisperers", 235 | "group": "Hobby Action Coalition (HAC)" 236 | }, 237 | { 238 | "code": "99WinterStyling", 239 | "description": "Cohort for who people who style", 240 | "group": "Hobby Action Coalition (HAC)" 241 | }, 242 | { 243 | "code": "99WinterTriviaing", 244 | "description": "Cohort for who people who trivia", 245 | "group": "Hobby Action Coalition (HAC)" 246 | }, 247 | { 248 | "code": "99WinterSocializing", 249 | "description": "Cohort for people who socialize", 250 | "group": "Hobby Action Coalition (HAC)" 251 | }, 252 | { 253 | "code": "99WinterJoking", 254 | "description": "Cohort for jokers", 255 | "group": "Hobby Action Coalition (HAC)" 256 | } 257 | ] -------------------------------------------------------------------------------- /seumich/templates/seumich/student_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'seumich/student.html' %} 2 | {% load filters cache %} 3 | 4 | {% block head %} 5 | {% load static %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | {% cache settings.CACHE_TTL student_detail request.get_full_path %} 11 |
12 |
13 |
14 | {% include 'seumich/student_info_partial.html' with link_present=False %} 15 |
16 |
17 |
18 |

Courses Summary

19 |
20 | 108 |
109 | 110 | Advisors: 111 | 112 | 113 | {% for element in advisors %} 114 | {% with mentor=element.mentor cohort=element.cohort %} 115 |

116 | {{ mentor.first_name }} 117 | {{ mentor.last_name }} 118 | 119 |

120 | {% endwith %} 121 | {% endfor %} 122 |
123 |
124 |
125 | 126 | {% endcache %} 127 | {% endblock %} 128 | -------------------------------------------------------------------------------- /student_explorer/templates/student_explorer/index.html: -------------------------------------------------------------------------------- 1 | {% load flatpages %} 2 | {% get_flatpages as flatpages %} 3 | 4 | 5 | 6 | 7 | {% if request.path != '/admin/' and settings.GA4_CONFIG_ID %} {# Exclude admin pages from tracking #} 8 | 9 | 10 | 16 | {% endif %} 17 | {% load static %} 18 | 19 | Student Explorer 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% block head %}{% endblock %} 40 | 41 | 42 | 43 | 126 | {% for page in flatpages %} 127 | {% if page.url == "/user-comm-banner/" %} 128 | {{page.content|safe}} 129 | {% endif %} 130 | {% endfor %} 131 |
132 | {% if messages %} 133 |
134 | {% for message in messages %} 135 | 141 | {% endfor %} 142 |
143 | {% endif %} 144 | {% block content %}{% endblock %} 145 | 165 |
166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /management/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect, get_object_or_404 2 | from django.contrib.auth.decorators import login_required 3 | from django.utils.decorators import method_decorator 4 | from django.core.exceptions import PermissionDenied 5 | from django.views.generic import TemplateView, ListView, View 6 | from django.views.generic.edit import FormView, CreateView 7 | from django.http import StreamingHttpResponse, HttpResponse 8 | from django.conf import settings 9 | 10 | from management.forms import CohortForm, UserCreateForm 11 | 12 | from .models import Student, Mentor, Cohort, StudentCohortMentor 13 | 14 | import csv 15 | import xlrd 16 | import xlwt 17 | 18 | 19 | from django.contrib.auth import get_user_model 20 | User = get_user_model() 21 | 22 | 23 | class StaffRequiredMixin(object): 24 | 25 | @method_decorator(login_required) 26 | def dispatch(self, request, *args, **kwargs): 27 | if not request.user.is_staff: 28 | raise PermissionDenied 29 | return super(StaffRequiredMixin, self).dispatch(request, 30 | *args, **kwargs) 31 | 32 | 33 | class StaffOrTokenRequiredMixin(object): 34 | 35 | @method_decorator(login_required) 36 | def dispatch(self, request, *args, **kwargs): 37 | token = request.GET.get('token', None) 38 | if not token or token != settings.DOWNLOAD_TOKEN: 39 | if not request.user.is_staff: 40 | raise PermissionDenied 41 | return super(StaffOrTokenRequiredMixin, self).dispatch(request, 42 | *args, **kwargs) 43 | 44 | 45 | class IndexView(StaffRequiredMixin, TemplateView): 46 | template_name = 'management/index.html' 47 | 48 | 49 | class CohortListView(StaffRequiredMixin, ListView): 50 | template_name = 'management/cohort_list.html' 51 | model = Cohort 52 | 53 | def get_context_data(self, **kwargs): 54 | context = super(CohortListView, self).get_context_data(**kwargs) 55 | context['query_term'] = self.all_cohorts 56 | context['show'] = 'all' if self.all_cohorts == 'all' else 'active' 57 | return context 58 | 59 | def get(self, request, *args, **kwargs): 60 | return super(CohortListView, self).get(request, *args, **kwargs) 61 | 62 | def get_queryset(self): 63 | self.all_cohorts = self.request.GET.get('show', None) 64 | if self.all_cohorts and self.all_cohorts == 'all': 65 | cohort_list = self.model.objects.all() 66 | else: 67 | cohort_list = self.model.objects.filter(active=True) 68 | return cohort_list 69 | 70 | def post(self, request, *args, **kwargs): 71 | code = request.POST.get('code', None) 72 | action = request.POST.get('action', None) 73 | if code and action: 74 | if action == 'activate': 75 | instance = get_object_or_404(Cohort, code=code) 76 | instance.active = True 77 | instance.save() 78 | if action == 'deactivate': 79 | instance = get_object_or_404(Cohort, code=code) 80 | instance.active = False 81 | instance.save() 82 | if action == 'delete': 83 | instance = get_object_or_404(Cohort, code=code) 84 | instance.delete(request = request) 85 | return self.get(request, *args, **kwargs) 86 | 87 | 88 | class UserListView(StaffRequiredMixin, ListView): 89 | template_name = 'management/user_list.html' 90 | model = User 91 | paginate_by = 50 92 | 93 | def get_context_data(self, **kwargs): 94 | context = super(UserListView, self).get_context_data(**kwargs) 95 | context['query_term'] = self.all_users 96 | context['show'] = 'all' if self.all_users == 'all' else 'active' 97 | return context 98 | 99 | def get(self, request, *args, **kwargs): 100 | return super(UserListView, self).get(request, *args, **kwargs) 101 | 102 | def get_queryset(self): 103 | self.all_users = self.request.GET.get('show', None) 104 | if self.all_users and self.all_users == 'all': 105 | user_list = self.model.objects.all() 106 | else: 107 | user_list = self.model.objects.filter(is_active=True) 108 | return user_list 109 | 110 | def post(self, request, *args, **kwargs): 111 | username = request.POST.get('username', None) 112 | action = request.POST.get('action', None) 113 | if username and action: 114 | if action == 'activate': 115 | instance = get_object_or_404(User, username=username) 116 | instance.is_active = True 117 | instance.save() 118 | if action == 'deactivate': 119 | instance = get_object_or_404(User, username=username) 120 | instance.is_active = False 121 | instance.save() 122 | return self.get(request, *args, **kwargs) 123 | 124 | 125 | class CohortMembersView(StaffRequiredMixin, ListView): 126 | template_name = 'management/cohort_members.html' 127 | model = StudentCohortMentor 128 | paginate_by = 50 129 | 130 | def get_context_data(self, **kwargs): 131 | context = super(CohortMembersView, self).get_context_data(**kwargs) 132 | context['cohort_code'] = self.kwargs['code'] 133 | return context 134 | 135 | def get_queryset(self): 136 | return (self.model.objects 137 | .filter(cohort__code=self.kwargs['code']) 138 | .prefetch_related('student', 139 | 'cohort', 140 | 'mentor')) 141 | 142 | 143 | class AddCohortView(StaffRequiredMixin, FormView): 144 | template_name = 'management/cohort_add.html' 145 | form_class = CohortForm 146 | success_url = '/manage/cohorts/' 147 | 148 | def process_form_members(self, form, members): 149 | sniffer = csv.Sniffer() 150 | members = members.split('\r\n') 151 | dialect = sniffer.sniff(members[0]) 152 | delimiter = dialect.delimiter 153 | for member in members: 154 | record = member.split(delimiter) 155 | student, created = Student.objects.get_or_create( 156 | username=record[0].strip()) 157 | mentor, created = Mentor.objects.get_or_create( 158 | username=record[1].strip()) 159 | cohort = form.save() 160 | (StudentCohortMentor 161 | .objects 162 | .get_or_create(student=student, 163 | cohort=cohort, 164 | mentor=mentor 165 | )) 166 | 167 | def handle_uploaded_file(self, form, fname): 168 | xl_workbook = xlrd.open_workbook(file_contents=fname.read()) 169 | xl_sheet = xl_workbook.sheet_by_index(0) 170 | for row_idx in range(0, xl_sheet.nrows): 171 | student, created = Student.objects.get_or_create( 172 | username=xl_sheet.cell(row_idx, 0).value.strip()) 173 | mentor, created = Mentor.objects.get_or_create( 174 | username=xl_sheet.cell(row_idx, 1).value.strip()) 175 | cohort = form.save() 176 | (StudentCohortMentor 177 | .objects 178 | .get_or_create(student=student, 179 | cohort=cohort, 180 | mentor=mentor 181 | )) 182 | 183 | def post(self, request, *args, **kwargs): 184 | form = self.form_class(request.POST, request.FILES) 185 | if form.is_valid(): 186 | members = form.cleaned_data['members'] 187 | if members: 188 | self.process_form_members(form, members) 189 | if len(request.FILES) != 0: 190 | self.handle_uploaded_file(form, request.FILES['excel_file']) 191 | return redirect(self.success_url) 192 | return self.render_to_response(self.get_context_data(**kwargs)) 193 | 194 | 195 | class AddUserView(StaffRequiredMixin, CreateView): 196 | template_name = 'management/user_add.html' 197 | form_class = UserCreateForm 198 | success_url = '/manage/users/' 199 | 200 | 201 | class CohortListDownloadView(StaffOrTokenRequiredMixin, View): 202 | 203 | class Echo(object): 204 | 205 | def write(self, value): 206 | return value 207 | 208 | def iter_qs(self, rows, header, file_obj): 209 | writer = csv.writer(file_obj, delimiter='\t') 210 | yield writer.writerow(header) 211 | for row in rows: 212 | yield writer.writerow(row) 213 | 214 | def render_to_csv(self, header, rows, fname): 215 | """A function that streams a large CSV file.""" 216 | response = (StreamingHttpResponse( 217 | self.iter_qs(rows, 218 | header, 219 | self.Echo()), 220 | content_type="text/tab-separated-values")) 221 | response['Content-Disposition'] = 'attachment; filename="%s"' % fname 222 | return response 223 | 224 | def get(self, request, *args, **kwargs): 225 | headers = ('CohortCode', 'CohortDescription', 'CohortGroup') 226 | rows = (Cohort.objects 227 | .filter(active=True) 228 | .values_list('code', 229 | 'description', 230 | 'group')) 231 | return self.render_to_csv(headers, 232 | rows, 233 | 'TLA_Cohort_USELAB.dat') 234 | 235 | 236 | class CohortDetailDownloadView(CohortListDownloadView): 237 | 238 | def get(self, request, *args, **kwargs): 239 | headers = ('StudentUniqname', 'CohortCode', 'MentorUniqname') 240 | rows = (StudentCohortMentor.objects 241 | .filter(cohort__active=True) 242 | .values_list('student__username', 243 | 'cohort__code', 244 | 'mentor__username').order_by('id')) 245 | return self.render_to_csv(headers, 246 | rows, 247 | 'TLA_StudentCohortMentor_USELAB.dat') 248 | 249 | 250 | class CohortMembersDownloadView(CohortListDownloadView): 251 | 252 | def render_to_excel(self, rows, fname): 253 | response = HttpResponse(content_type="application/ms-excel") 254 | response['Content-Disposition'] = 'attachment; filename="%s"' % fname 255 | 256 | wb = xlwt.Workbook() 257 | ws = wb.add_sheet('Students') 258 | 259 | for idx, row in enumerate(rows): 260 | ws.write(idx, 0, row[0]) 261 | ws.write(idx, 1, row[1]) 262 | 263 | wb.save(response) 264 | return response 265 | 266 | def get(self, request, *args, **kwargs): 267 | rows = (StudentCohortMentor.objects 268 | .filter(cohort__code=self.kwargs['code']) 269 | .values_list('student__username', 270 | 'mentor__username').order_by('id')) 271 | return self.render_to_excel( 272 | rows, 273 | 'StudentCohortMentor_' + self.kwargs['code'] + '.xls' 274 | ) 275 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /seumich/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import View, ListView, TemplateView 2 | from seumich.models import (Student, Mentor, Cohort, ClassSite, 3 | ClassSiteScore, 4 | StudentCohortMentor, 5 | StudentClassSiteStatus, 6 | StudentClassSiteScore) 7 | from django.shortcuts import get_object_or_404, redirect 8 | from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned 9 | from django.db.models import Q, Prefetch, Value as V 10 | from django.db.models.functions import Concat 11 | from django.contrib.auth import get_user_model 12 | from django.contrib.auth.mixins import LoginRequiredMixin 13 | from django.contrib import messages 14 | from tracking.utils import UserLogPageViewMixin 15 | 16 | import operator 17 | import logging 18 | 19 | from decouple import config 20 | from functools import reduce 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class AdvisorsListView(LoginRequiredMixin, UserLogPageViewMixin, ListView): 26 | template_name = 'seumich/advisor_list.html' 27 | # Filtering for id >= 0 eliminates "Bad Value"-type results. 28 | queryset = Mentor.objects.filter(id__gte=0).order_by('last_name') 29 | context_object_name = 'advisors' 30 | 31 | 32 | class CohortsListView(LoginRequiredMixin, UserLogPageViewMixin, ListView): 33 | template_name = 'seumich/cohort_list.html' 34 | # Filtering for id >= 0 eliminates "Bad Value"-type results. 35 | queryset = Cohort.objects.filter(id__gte=0) 36 | context_object_name = 'cohorts' 37 | 38 | class StudentsListView(LoginRequiredMixin, UserLogPageViewMixin, ListView): 39 | template_name = 'seumich/student_list.html' 40 | context_object_name = 'students' 41 | 42 | def get(self, request): 43 | univ_id = self.request.GET.get('univ_id', None) 44 | if univ_id: 45 | try: 46 | student = get_object_or_404(Student, univ_id=univ_id) 47 | return redirect('seumich:student', student.username) 48 | except MultipleObjectsReturned: 49 | logger.info('Multiple students with the same univ_id (%s)' 50 | % univ_id) 51 | return super(StudentsListView, self).get(request) 52 | 53 | def get_context_data(self, **kwargs): 54 | context = super(StudentsListView, self).get_context_data(**kwargs) 55 | context['studentListHeader'] = 'Search Students' 56 | context['query_user'] = self.query_user 57 | return context 58 | 59 | def get_queryset(self): 60 | self.query_user = self.request.GET.get('search', None) 61 | self.univ_id = self.request.GET.get('univ_id', None) 62 | student_list = [] 63 | if self.query_user: 64 | # Filtering for id >= 0 eliminates "Bad Value"-type results. 65 | q_list = [Q(username__icontains=self.query_user)] 66 | q_list += [Q(univ_id__icontains=self.query_user)] 67 | q_list += [Q(first_name__icontains=self.query_user)] 68 | q_list += [Q(last_name__icontains=self.query_user)] 69 | q_list += [Q(full_name__icontains=self.query_user)] 70 | student_list = (Student.objects.filter(id__gte=0) 71 | .annotate(full_name=(Concat('first_name', V(' '), 'last_name'))) 72 | .filter(reduce(operator.or_, q_list)) 73 | .order_by('last_name').distinct()) 74 | student_list = student_list.prefetch_related( 75 | 'studentclasssitestatus_set__status', 76 | 'studentclasssitestatus_set__class_site', 77 | 'cohorts') 78 | elif self.univ_id: 79 | student_list = Student.objects.filter(id__gte=0).filter( 80 | univ_id=self.univ_id) 81 | student_list = student_list.prefetch_related( 82 | 'studentclasssitestatus_set__status', 83 | 'studentclasssitestatus_set__class_site', 84 | 'cohorts') 85 | messages.add_message( 86 | self.request, 87 | messages.WARNING, 88 | 'Multiple students with the same univ_id (%s)' % self.univ_id) 89 | return student_list 90 | 91 | 92 | class AdvisorView(LoginRequiredMixin, UserLogPageViewMixin, ListView): 93 | template_name = 'seumich/advisor_detail.html' 94 | context_object_name = 'students' 95 | 96 | def get_context_data(self, **kwargs): 97 | context = super(AdvisorView, self).get_context_data(**kwargs) 98 | if self.mentor is not None: 99 | context['studentListHeader'] = " ".join([self.mentor.first_name, self.mentor.last_name]) 100 | context['advisor'] = self.mentor 101 | else: 102 | try: 103 | user = get_user_model().objects.get(username=self.kwargs['advisor']) 104 | context['studentListHeader'] = " ".join([user.first_name, user.last_name]) 105 | context['advisor'] = user 106 | except ObjectDoesNotExist: 107 | context['studentListHeader'] = '' 108 | context['advisor'] = None 109 | return context 110 | 111 | def get_queryset(self): 112 | self.mentor = None 113 | try: 114 | self.mentor = Mentor.objects.get(username=self.kwargs['advisor']) 115 | student_list = self.mentor.students.filter(id__gte=0).order_by('last_name').distinct() 116 | student_list = student_list.prefetch_related( 117 | 'studentclasssitestatus_set__status', 118 | 'studentclasssitestatus_set__class_site', 119 | 'cohorts' 120 | ) 121 | except ObjectDoesNotExist: 122 | student_list = [] 123 | return student_list 124 | 125 | 126 | class CohortView(LoginRequiredMixin, UserLogPageViewMixin, ListView): 127 | template_name = 'seumich/cohort_detail.html' 128 | context_object_name = 'students' 129 | 130 | def get_context_data(self, **kwargs): 131 | context = super(CohortView, self).get_context_data(**kwargs) 132 | context['studentListHeader'] = self.cohort.description 133 | context['cohort'] = self.cohort 134 | return context 135 | 136 | def get_queryset(self): 137 | self.cohort = get_object_or_404(Cohort, code=self.kwargs['code']) 138 | student_list = Student.objects.filter( 139 | studentcohortmentor__cohort=self.cohort).filter( 140 | id__gte=0).distinct() 141 | student_list = student_list.prefetch_related( 142 | 'studentclasssitestatus_set__status', 143 | 'studentclasssitestatus_set__class_site', 144 | 'cohorts') 145 | return student_list 146 | 147 | 148 | class ClassSiteView(LoginRequiredMixin, UserLogPageViewMixin, ListView): 149 | template_name = 'seumich/class_site_detail.html' 150 | context_object_name = 'students' 151 | 152 | def get_context_data(self, **kwargs): 153 | context = super(ClassSiteView, self).get_context_data(**kwargs) 154 | context['studentListHeader'] = self.class_site.description 155 | context['class_site'] = self.class_site 156 | return context 157 | 158 | def get_queryset(self): 159 | self.class_site = get_object_or_404(ClassSite, 160 | id=self.kwargs['class_site_id']) 161 | student_list = Student.objects.filter( 162 | studentclasssitestatus__class_site=self.class_site).filter( 163 | id__gte=0).distinct() 164 | student_list = student_list.prefetch_related( 165 | 'studentclasssitestatus_set__status', 166 | 'studentclasssitestatus_set__class_site', 167 | 'cohorts') 168 | return student_list 169 | 170 | 171 | class IndexView(LoginRequiredMixin, UserLogPageViewMixin, View): 172 | 173 | def get(self, request): 174 | return redirect('seumich:advisor', advisor=request.user.username) 175 | 176 | 177 | class StudentView(LoginRequiredMixin, UserLogPageViewMixin, TemplateView): 178 | template_name = 'seumich/student_detail.html' 179 | 180 | def get_context_data(self, student, **kwargs): 181 | context = super(StudentView, self).get_context_data(**kwargs) 182 | selected_student = get_object_or_404( 183 | Student.objects.prefetch_related('studentclasssitestatus_set__status'), 184 | username=student) 185 | context['student'] = selected_student 186 | prefetch_student_score = Prefetch( 187 | 'class_site__studentclasssitescore_set', 188 | queryset=StudentClassSiteScore.objects.filter( 189 | student=selected_student)) 190 | context['classSites'] = (StudentClassSiteStatus.objects 191 | .filter( 192 | student=selected_student 193 | ) 194 | .prefetch_related( 195 | 'class_site__classsitescore_set', 196 | 'status', 197 | prefetch_student_score 198 | )) 199 | context['advisors'] = (StudentCohortMentor.objects 200 | .filter( 201 | student=selected_student) 202 | .prefetch_related('mentor', 'cohort')) 203 | return context 204 | 205 | 206 | class StudentClassSiteView(StudentView): 207 | template_name = 'seumich/student_class_site_detail.html' 208 | 209 | def get_class_history(self, student, class_site, format=None): 210 | 211 | studentData = [] 212 | classData = [] 213 | activityData = [] 214 | 215 | try: 216 | term = class_site.terms.get() 217 | except: 218 | return studentData, classData, activityData 219 | 220 | events = {x.get('week_end_date_id'):x 221 | for x in class_site.weeklystudentclasssiteevent_set.filter( 222 | student=student).values()} 223 | student_scores = {x.get('week_end_date_id'):x 224 | for x in class_site.weeklystudentclasssitescore_set.filter( 225 | student=student).values()} 226 | class_scores = {x.get('week_end_date_id'):x 227 | for x in class_site.weeklyclasssitescore_set.all().values()} 228 | todays_week_end_date = term.todays_week_end_date() 229 | 230 | week_number = 0 231 | 232 | for week_end_date in term.week_end_dates(): 233 | tempStudentData = [] 234 | tempClassData = [] 235 | tempActivityData = [] 236 | week_number += 1 237 | 238 | tempStudentData.append(week_number) 239 | tempClassData.append(week_number) 240 | tempActivityData.append(week_number) 241 | 242 | event = events.get(week_end_date.id) 243 | if event: 244 | tempActivityData.append(round(event.get('percentile_rank') * 100)) 245 | 246 | score = student_scores.get(week_end_date.id) 247 | if score: 248 | tempStudentData.append(score.get('score')) 249 | 250 | score = class_scores.get(week_end_date.id) 251 | if score: 252 | tempClassData.append(score.get('score')) 253 | 254 | if week_end_date == todays_week_end_date: 255 | tempStudentData.append(student.studentclasssitescore_set 256 | .get(class_site=class_site) 257 | .current_score_average) 258 | 259 | class_site_score = ClassSiteScore.objects.get( 260 | class_site__code=class_site.code) 261 | tempClassData.append(class_site_score.current_score_average) 262 | 263 | studentData.append(tempStudentData) 264 | classData.append(tempClassData) 265 | activityData.append(tempActivityData) 266 | 267 | return studentData, classData, activityData 268 | 269 | def get_context_data(self, student, classcode, **kwargs): 270 | context = super(StudentClassSiteView, self).get_context_data( 271 | student, **kwargs) 272 | student = get_object_or_404(Student, username=student) 273 | class_site = get_object_or_404(ClassSite, code=classcode) 274 | studentData, classData, activityData = self.get_class_history( 275 | student, class_site) 276 | 277 | scoreData = [] 278 | eventPercentileData = [] 279 | scoreData.append( 280 | {'key': 'Student', 'values': studentData, 'color': '#255c91'}) 281 | scoreData.append( 282 | {'key': 'Class', 'values': classData, 'color': '#F0D654'}) 283 | eventPercentileData.append( 284 | { 285 | 'key': 'Course Site Engagement', 286 | 'values': activityData, 'color': '#a9bdab' 287 | }) 288 | 289 | context['classSite'] = class_site 290 | context['scoreData'] = scoreData 291 | context['eventPercentileData'] = eventPercentileData 292 | context['assignments'] = student.studentclasssiteassignment_set.filter( 293 | class_site=class_site).prefetch_related('assignment', '_due_date') 294 | context['current_status'] = student.studentclasssitestatus_set.get( 295 | class_site=class_site).status.description 296 | 297 | course_url_prefix = config("CANVAS_COURSE_URL_PREFIX", default="") 298 | logger.info("course_url_prefix " + course_url_prefix) 299 | if (course_url_prefix != ""): 300 | context['class_site_canvas_url'] = course_url_prefix + class_site.code 301 | 302 | logger.info("class_site_canvas_url " + course_url_prefix + class_site.code) 303 | return context 304 | --------------------------------------------------------------------------------