├── 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 |Sorry, your browser sent a request that this server could not understand.
11 |Sorry, the server encountered an internal error and was unable to complete your request.
11 |Sorry, but the page you were trying to view does not exist.
11 |It looks like this was the result of either:
12 |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 |/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 |
29 |
30 |
31 | Student Explorer Management
32 |
33 |
34 |
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 | Student Username
13 | Cohort Code
14 | Advisor Username
15 |
16 |
17 |
18 | {% for object in object_list %}
19 |
20 | {{ object.student.username }}
21 | {{ object.cohort.code }}
22 | {{ object.mentor.username }}
23 |
24 | {% empty %}
25 |
26 | None Found
27 |
28 | {% endfor %}
29 |
30 |
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 |
25 | {{ cohort.description }}
26 |
27 |
28 | {% endfor %}
29 |
30 |
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 |
--------------------------------------------------------------------------------
/feedback/templates/feedback/feedback.html:
--------------------------------------------------------------------------------
1 | {% extends 'student_explorer/index.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
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 |
25 | {% url 'seumich:advisor' advisor.username as advisor_url %}
26 | {% if advisor_url %}
27 | {{ advisor.first_name }}
28 | {% endif %}
29 |
30 |
31 | {% url 'seumich:advisor' advisor.username as advisor_url %}
32 | {% if advisor_url %}
33 | {{ advisor.last_name }}
34 | {% endif %}
35 |
36 |
37 | {% url 'seumich:advisor' advisor.username as advisor_url %}
38 | {% if advisor_url %}
39 | {{advisor.username}}
40 | {% endif %}
41 |
42 |
43 | {% endfor %}
44 |
45 |
46 | {% include 'seumich/ts_pager.html' with colspan="3" %}
47 |
48 |
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 |
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 |
19 |
20 |
21 | Username
22 | First name
23 | Last name
24 | Action
25 |
26 |
27 |
28 | {% for user in object_list %}
29 |
30 |
31 | {% if user.is_active %}
32 | {{ user.username }}
33 | {% else %}
34 | {{ user.username }}
35 | {% endif %}
36 |
37 | {{ user.first_name }}
38 | {{ user.last_name }}
39 |
40 | Lookup
41 | |
42 |
43 | {% if user.is_active %}
44 | Deactivate
45 | {% else %}
46 | Activate
47 | {% endif %}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
79 |
80 |
81 |
82 |
83 |
84 | {% empty %}
85 |
86 | None Found
87 |
88 | {% endfor %}
89 |
90 |
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 |
63 | {{ student.first_name }}
64 |
65 |
66 | {{ student.last_name }}
67 |
68 |
69 | {{ student.username }}
70 |
71 |
72 |
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 |
80 |
81 | {% elif status.description == 'Yellow' %}
82 |
83 |
84 |
85 | {% elif status.description == 'Red' %}
86 |
87 |
88 |
89 | {% elif status.description == 'Not Applicable' %}
90 |
91 |
92 |
93 | {% endif %}
94 | {% endwith %}
95 |
96 | {% endwith %}
97 | {% endfor %}
98 |
99 |
104 |
105 | {% endfor %}
106 |
107 |
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 |
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 |
34 |
35 |
36 | Code
37 | Description
38 | Group
39 | Action
40 |
41 |
42 |
43 |
44 | Code
45 | Description
46 | Group
47 | Action
48 |
49 |
50 | {% include 'seumich/ts_pager.html' with colspan="6" %}
51 |
52 |
53 |
54 | {% for cohort in object_list %}
55 |
56 |
57 |
58 | {% if cohort.active %}
59 | {{ cohort.code }}
60 | {% else %}
61 | {{ cohort.code }}
62 | {% endif %}
63 |
64 |
65 | {{ cohort.description }}
66 | {{ cohort.group }}
67 |
68 |
69 |
70 | {% if cohort.active %}
71 | Deactivate
72 | {% else %}
73 | Activate
74 | {% endif %}
75 |
76 | |
77 |
78 | Delete
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
137 |
138 |
139 |
140 |
141 |
142 | {% empty %}
143 |
144 | None Found
145 |
146 | {% endfor %}
147 |
148 |
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 |
17 |
18 | Courses Summary
19 |
20 |
21 |
22 | {% if not classSites %}
23 |
24 | There is no class site data for this student.
25 |
26 | {% else %}
27 |
28 |
39 |
40 |
41 |
42 |
43 |
44 | Student
45 |
46 |
47 |
48 | Class Average
49 |
50 |
51 |
52 |
53 | {% for element in classSites %}
54 |
55 | {% with class_site=element.class_site %}
56 |
57 |
58 |
59 | {{ class_site.description }}
60 |
61 |
82 |
83 | {% with student_score=class_site.studentclasssitescore_set.all|get_score class_score=class_site.classsitescore_set.all|get_score %}
84 |
91 |
98 | {% endwith %}
99 |
100 |
101 |
102 | {% endwith %}
103 | {% endfor %}
104 |
105 | {% endif %}
106 |
107 |
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 |
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 |
--------------------------------------------------------------------------------