diff --git a/README.md b/README.md index d4febf34..33523208 100644 --- a/README.md +++ b/README.md @@ -29,15 +29,17 @@ The web app is built on Django, and uses an SQLite database. [Click here](https: 5. Clone this project from Github -6. Copy settings.py from opra_dependencies to compsocsite/compsocsite. Copy opra_crypto.py from opra_dependencies to compsocsite/polls. +6. ~~Copy settings.py from opra_dependencies to compsocsite/compsocsite.~~ (an updated settings.py is now included.) Copy opra_crypto.py from opra_dependencies to compsocsite/polls. + +6.5. For testing locally, edit compsocsite/appauth/views.py and find-replace `https://opra.cs.rpi.edu` with `http://127.0.0.1:8000` 7. Open command line (terminal), change to OPRA's directory, and then enter the following commands: - cd composcite - - python manage.py migrate - - python manage.py createcachetable + ``` + cd composcite + python manage.py migrate + python manage.py createcachetable + ``` Then run the server by entering: @@ -58,6 +60,7 @@ The web app is built on Django, and uses an SQLite database. [Click here](https: * **scipy**: * **numpy**: * **networkx**: +* **django-crispy-forms**: * **django-mathfilters**: * **matplotlib**: diff --git a/compsocsite/appauth/migrations/0004_userprofile_showhint.py b/compsocsite/appauth/migrations/0004_userprofile_showhint.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0005_auto_20170306_1628.py b/compsocsite/appauth/migrations/0005_auto_20170306_1628.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0007_userprofile_code.py b/compsocsite/appauth/migrations/0007_userprofile_code.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0008_userprofile_comments.py b/compsocsite/appauth/migrations/0008_userprofile_comments.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0009_auto_20171204_1448.py b/compsocsite/appauth/migrations/0009_auto_20171204_1448.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0010_auto_20171216_1342.py b/compsocsite/appauth/migrations/0010_auto_20171216_1342.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0011_auto_20171224_2353.py b/compsocsite/appauth/migrations/0011_auto_20171224_2353.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0012_auto_20171224_2356.py b/compsocsite/appauth/migrations/0012_auto_20171224_2356.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0013_auto_20171229_1132.py b/compsocsite/appauth/migrations/0013_auto_20171229_1132.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0014_auto_20171229_1135.py b/compsocsite/appauth/migrations/0014_auto_20171229_1135.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0015_userprofile_finished.py b/compsocsite/appauth/migrations/0015_userprofile_finished.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0016_userprofile_numq.py b/compsocsite/appauth/migrations/0016_userprofile_numq.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0017_userprofile_exp_data.py b/compsocsite/appauth/migrations/0017_userprofile_exp_data.py old mode 100644 new mode 100755 diff --git a/compsocsite/appauth/migrations/0018_auto_20191125_0156.py b/compsocsite/appauth/migrations/0018_auto_20191125_0156.py new file mode 100644 index 00000000..dbfb372a --- /dev/null +++ b/compsocsite/appauth/migrations/0018_auto_20191125_0156.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2 on 2019-11-25 06:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mentors', '0001_initial'), + ('appauth', '0017_userprofile_exp_data'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='mentor_applied', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='userprofile', + name='mentor_profile', + field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mentors.Mentor'), + ), + ] diff --git a/compsocsite/appauth/models.py b/compsocsite/appauth/models.py index 79b59ff1..d3271cd1 100755 --- a/compsocsite/appauth/models.py +++ b/compsocsite/appauth/models.py @@ -2,6 +2,8 @@ import datetime import django + +from mentors.models import Mentor from django import forms from django.db import models from django.utils import timezone @@ -10,7 +12,7 @@ class UserProfile(models.Model): # This line is required. Links UserProfile to a User model instance. - user = models.OneToOneField(User,on_delete=models.CASCADE,) + user = models.OneToOneField(User, on_delete = models.CASCADE,) time_creation = models.DateTimeField() displayPref = models.IntegerField(default=1) emailInvite = models.BooleanField(default=False) @@ -27,6 +29,13 @@ class UserProfile(models.Model): finished = models.BooleanField(default=False) numq= models.IntegerField(default=0) exp_data = models.TextField(default="{}") + + # Mentor Applicaton Profile + mentor_applied = models.BooleanField(default = False) + + # Set mentor reference to SET_NULL here to protect the deletion of the user profile + mentor_profile = models.OneToOneField(Mentor, on_delete = models.SET_NULL, default = None, null = True) + # Override the __unicode__() method to return out something meaningful! def __unicode__(self): return self.user.username diff --git a/compsocsite/appauth/templates/settings.html b/compsocsite/appauth/templates/settings.html index d8bdc70b..a434d01e 100755 --- a/compsocsite/appauth/templates/settings.html +++ b/compsocsite/appauth/templates/settings.html @@ -29,7 +29,7 @@
- +
diff --git a/compsocsite/compsocsite/settings.py b/compsocsite/compsocsite/settings.py new file mode 100755 index 00000000..abd4b3c5 --- /dev/null +++ b/compsocsite/compsocsite/settings.py @@ -0,0 +1,192 @@ +""" +Django settings for compsocsite project. + +Generated by 'django-admin startproject' using Django 1.9.5. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '_' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'polls.apps.PollsConfig', + 'crispy_forms', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'compsocsite', + 'appauth', + 'groups', + 'multipolls', + 'mentors', + 'django_mobile', + 'mathfilters', + 'sessions_local', + 'corsheaders', + 'qr_code', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django_mobile.middleware.MobileDetectionMiddleware', + 'django_mobile.middleware.SetFlavourMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', +] + +ROOT_URLCONF = 'compsocsite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'django_mobile.context_processors.flavour', + ], + 'loaders':( + ('django_mobile.loader.CachedLoader', ( + 'django_mobile.loader.Loader', + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'django.template.loaders.app_directories.Loader', + )), + ) + }, + }, +] + +WSGI_APPLICATION = 'compsocsite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', + 'LOCATION': 'my_cache_table', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +#CAS +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', + #'cas.backends.CASBackend', +] +CAS_GATEWAY = True + +CAS_RESPONSE_CALLBACKS = ( + 'module.callbackfunction', +) + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'EST' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +#CAS +CAS_SERVER_URL = "https://cas-auth.rpi.edu/cas/" +CAS_LOGOUT_COMPLETELY = True +CAS_PROVIDE_URL_TO_LOGOUT = True +CAS_AUTO_CREATE_USERS = True +CAS_IGNORE_REFERER = True +CAS_REDIRECT_URL = '/polls/regular_polls' +#'https://opra.cs.rpi.edu' +# CAS_FORCE_SSL_SERVICE_URL = True + +################################################# +# Email settings # +################################################# +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_USE_TLS = True +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_HOST_USER = 'oprahprogramtest@gmail.com' +EMAIL_HOST_PASSWORD = 'ThisIsJustATestProgram' +EMAIL_PORT = 587 + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' +LOGIN_URL = '/auth/login/' + +STATICFILES_DIRS = ( + '/static/', + os.path.join(os.path.abspath(BASE_DIR), 'static'), +) + + +# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +# SESSION_COOKIE_SECURE = True +# CSRF_COOKIE_SECURE = True +# SECURE_HSTS_SECONDS = 3600 +# SECURE_SSL_REDIRECT = False diff --git a/compsocsite/compsocsite/urls.py b/compsocsite/compsocsite/urls.py index 70b2ab15..6e8e0f52 100755 --- a/compsocsite/compsocsite/urls.py +++ b/compsocsite/compsocsite/urls.py @@ -34,6 +34,7 @@ url(r'^sessions/', include('sessions_local.urls')), url(r'^static/(?P.*)$', serve, {'document_root': settings.MEDIA_ROOT, 'show_indexes':True}), url(r'^multipolls/', include('multipolls.urls')), + url(r'^mentors/', include('mentors.urls')), url(r'^GM2017$', GMView.as_view(), name='voting_demo'), url(r'^message$', sendMessage, name='message'), url(r'^GM2017$', GMView.as_view(), name='GM_2017'), diff --git a/compsocsite/mentors/__init__.py b/compsocsite/mentors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/compsocsite/mentors/admin.py b/compsocsite/mentors/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/compsocsite/mentors/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/compsocsite/mentors/apps.py b/compsocsite/mentors/apps.py new file mode 100644 index 00000000..5649da65 --- /dev/null +++ b/compsocsite/mentors/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MentorsConfig(AppConfig): + name = 'mentors' diff --git a/compsocsite/mentors/forms.py b/compsocsite/mentors/forms.py new file mode 100644 index 00000000..8e7033b2 --- /dev/null +++ b/compsocsite/mentors/forms.py @@ -0,0 +1,593 @@ +from django import forms +from .models import * +from django.forms import ModelForm +from django.utils.translation import gettext_lazy as _ + +# import crispy form plugin +from crispy_forms.helper import FormHelper +from crispy_forms.layout import * +from crispy_forms.bootstrap import * + +class MentorApplicationfoForm_step1(ModelForm): + class Meta: + model = Mentor + #fields = '__all__' + fields = ( 'RIN', + 'first_name', + 'last_name', + 'GPA', + 'email', + 'phone', + 'recommender', + ) + widgets = { + 'RIN': forms.TextInput(attrs={'placeholder': ' 661680100'}), + 'GPA': forms.TextInput(attrs={'placeholder': ' 3.6'}), + 'email': forms.TextInput(attrs={'readonly':'readonly', "style": "color:blue;"}), # Read-only email + 'phone': forms.TextInput(attrs={'placeholder': ' 5185941234'}), + } + def __init__(self, *args, **kwargs): + super(MentorApplicationfoForm_step1, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.fields['recommender'].required = False + self.helper.layout = Layout( + HTML('''

PERSONAL INFORMATION

'''), + HTML("""
"""), + + HTML("
"), + Div('RIN', css_class = "inputline"), + HTML("""
"""), + Div('first_name', css_class = "inputline"), + HTML("""
"""), + Div('last_name', css_class = "inputline"), + HTML("""
"""), + + Div('email',css_class = "inputline"), + HTML("""
"""), + + Div('phone', css_class = "inputline"), + HTML("""
"""), + + Div('GPA', css_class = "inputline"), + HTML("""
"""), + HTML("""
"""), + HTML(""" +
Please provide the name of someone in the CS Department who can recommend you. You are encouraged, but not required, to contact this person. + Please provide only a name here (describe additional circumstances in the freeform textbox below).
+ """), + Div('recommender', css_class = "inputline"), + HTML("
"), + ) + self.helper.form_method = 'POST' + self.helper.label_class = "inputline" + self.helper.add_input(Submit('next', 'Next')) + +class MentorApplicationfoForm_step2(ModelForm): + class Meta: + model = Mentor + #fields = '__all__' + fields = ( 'compensation', 'studnet_status', 'employed_paid_before') + def __init__(self, *args, **kwargs): + super(MentorApplicationfoForm_step2, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + HTML('''

COMPENSATION AND RESPONSIBILITIES

'''), + HTML("""
"""), + + HTML("
"), + HTML(""" +
+ We currently have funding for a fixed number of paid programming mentors. Therefore, if you apply for pay, you may be asked to work for credit instead. Course credit counts as a graded 1-credit or 2-credit free elective. In general, if you work an average of 1-3 hours per week, you will receive 1 credit; if you average 4 or more hours per week, you will receive 2 credits. Note that no more than 4 credits may be earned as an undergraduate (spanning all courses and all semesters). Please do not apply for credit if you have already earned 4 credits as a mentor; apply only for pay. +
+ """), + HTML("""
"""), + HTML("""
"""), + + HTML(""""""), + Div('compensation', css_class = "inputline"), + HTML("""
"""), + + HTML(""""""), + Div('employed_paid_before', css_class = "inputline"), + HTML("""
"""), + + HTML(""""""), + Div('studnet_status', css_class = "inputline"), + HTML("""
"""), + HTML("""
"""), + + HTML("""
"""), + HTML("""
For a paid position, you must follow the given instructions to ensure you are paid; otherwise, + another candidate will be selected. More specifically, you will be required to have a student employment card. This requires + you to have a federal I-9 on file. Bring appropriate identification with you to Amos Eaton 125 if this is your first time working for RPI; + click here for a list of acceptable documents. + Note that original documents are required; copies cannot be accepted.
"""), + + HTML("""
Detailed instructions are listed here: https://info.rpi.edu/student-employment/required-documents
"""), + + HTML("""
Please note that to be paid, you will be required to submit your specific hours in SIS + every two weeks. This must be completed to get paid on time, and any late submissions will incur a fee charged to the department.
"""), + + HTML("""
If you are unable to attend your assigned labs or office hours (e.g., illness, interview, conference, etc.), you must + notify your graduate lab TA and instructor and help arrange a substitute mentor. Unexcused absences will cause you to either earn a lower letter grade or not be asked to mentor again.
"""), + HTML("
"), + + ) + self.helper.form_method = 'POST' + self.helper.label_class = "my_label" + self.helper.form_show_labels = False + self.helper.add_input(Button('prev', 'Prev', onclick="window.history.go(-1); return false;")) + self.helper.add_input(Submit('next', 'Next')) + +class MentorApplicationfoForm_step3(ModelForm): + class Meta: + model = Mentor + fields = ( 'mentored_non_cs_bf',) + def __init__(self, *args, **kwargs): + super(MentorApplicationfoForm_step3, self).__init__(*args, **kwargs) + self.helper = FormHelper() + courses = Course.objects.all() + course_layout = Field() + for course in courses: + course_layout.append(HTML('''
''')) + course_layout.append(HTML('''
''' + str(course.name)+ "
") ) + course_layout.append(HTML('''
''' + str(course.subject+" "+course.number)+ "
") ) + + course_layout.append(HTML("     ")) + course_layout.append(HTML('''
''')) + grades = ( + ('a', 'A'), ('a-', 'A-'), + ('b+', 'B+'), ('b', 'B'), ('b-', 'B-'), + ('c+', 'C+'), ('c', 'C'), ('c-', 'C-'), + ('d+', 'D+'), ('d', 'D'), ('f', 'F'), + ('p', 'Progressing'), ('n', 'Not Taken'), + ) + grades_ap = ( + ('a', 'A'), ('a-', 'A-'), + ('b+', 'B+'), ('b', 'B'), ('b-', 'B-'), + ('c+', 'C+'), ('c', 'C'), ('c-', 'C-'), + ('d+', 'D+'), ('d', 'D'), ('f', 'F'), + ('p', 'Progressing'), ('ap', 'AP'), ('n', 'Not Taken'), + ) + choices_YN = ( + ('Y', 'YES'), + ('N', 'NO'), + ) + + self.fields[course.name+ "_grade"] = forms.ChoiceField( + choices = grades_ap if course.number == '1100' else grades, + label = "Grade on this course:", + ) + #self.initial[course.name+ "_grade"] = 'n' + self.fields[course.name+ "_exp"] = forms.ChoiceField(choices = choices_YN, label = "") + #self.initial[course.name+ "_exp"] = 'N' + course_layout.append(HTML('''
''')) + + course_layout.append(HTML("""

I have taken this course at RPI before and earned grade:  

""")) + course_layout.append(Field(course.name+ "_grade", label_class="float: left;")) + course_layout.append(HTML("""
""")) + course_layout.append(HTML("""

I have mentored this RPI course before:  

""")) + course_layout.append(Field(course.name+ "_exp")) + course_layout.append(HTML('''
''')) + + course_layout.append(HTML("""
""")), + + + self.helper.layout = Layout( + HTML('''

COURSE EXPERIENCE

'''), + HTML("""
"""), + + HTML("
"), + HTML('''
Below is a list of courses likely to need mentors. Note that CSCI 1100 is in + Python, CSCI 1190 is in MATLAB, CSCI 1200 is in C++, CSCI 2300 is in Python/C++, CSCI 2500 + is in C/Assembly, and CSCI 2600 is in Java. For each of the courses you have taken, + please specify the letter grade you earned in the course at RPI (or select "AP" if you + earned AP credit for CSCI 1100).
'''), + HTML('''
Note that you cannot be a mentor for a course you will be taking in the same semester or for a course + you plan to take in the future. In some cases, you are allowed to mentor for courses you have never taken; + be sure to describe equivalent courses or experiences in the general comments textbox further below. +
'''), + HTML("""
"""), + HTML(''''''), + Div('mentored_non_cs_bf'), + HTML("""
"""), + course_layout, + HTML("
"), + ) + self.helper.form_method = 'POST' + self.helper.form_show_labels = False + self.helper.add_input(Button('prev', 'Prev', onclick="window.history.go(-1); return false;")) + self.helper.add_input(Submit('next', 'Next')) + + +class MentorApplicationfoForm_step4(forms.Form): + def __init__(self, *args, **kwargs): + super(MentorApplicationfoForm_step4, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.fields["pref_order"] = forms.CharField(max_length=10000) + self.fields["pref_order"].required = False + + self.helper.layout = Layout( + HTML('''

COURSE PREFERENCE

'''), + HTML("""
"""), + HTML("
"), + HTML('''
Select the courses you would like to mentor and the courses + you do not prefer. For the course you would like to mentor, please change their rankings by + dragging the courses. + #1 means the highest prioity for you to mentor, and #2 means the second priority...etc. +
'''), + HTML('''
Your ranking will help us to decide your position. +
'''), + + HTML("""
"""), + + # Ranking UI here + HTML(''' +
+
+
+ Courses Prefer to Mentor:   + +
+ +
+
    + {% for course in pref_courses %} + {% if course %} +
      + +
      {{ forloop.counter }}
      +
    • +   {{ course.subject }} {{ course.number }}   {{course.name }} +
    • +
    + {% endif %} + {% endfor %} +
+
+
+
+
+
+
+ Courses NOT Prefer to Mentor:   + +
+ +
+
    + {% for course in not_pref_courses %} + {% if course %} +
      +
    • +   {{ course.subject }} {{ course.number }}   {{course.name }} +
    • +
    + {% endif %} + {% endfor %} +
+
+
+
+ '''), + # HTML part to store thr rankings + Field('pref_order', id='pref_order', css_class = 'pref_order', type = 'hidden'), + HTML("
"), + ) + self.helper.form_method = 'POST' + self.helper.add_input(Button('prev', 'Prev', onclick="window.history.go(-1); return false;")) + self.helper.add_input(Submit('next', 'Next', onclick="VoteUtil.submitPref();")) + +# Time slots availble for students +class MentorApplicationfoForm_step5(ModelForm): + class Meta: + model = Mentor + fields = ( 'other_times',) + widgets = { + #'time_slots': forms.CheckboxSelectMultiple(), + 'other_times': forms.Textarea(attrs={'cols': 100, 'rows': 8}) + } + + def __init__(self, *args, **kwargs): + super(MentorApplicationfoForm_step5, self).__init__(*args, **kwargs) + self.helper = FormHelper() + + time_slots_choices = ( + ('M_4:00-4:50PM', 'M 4:00-4:50PM'), + ('M_4:00-5:50PM', 'M 4:00-5:50PM'), + ('M_5:00-5:50PM', 'M 5:00-5:50PM'), + ('M_6:00-6:50PM', 'M 6:00-6:50PM'), + ('T_10:00-11:50AM', 'T 10:00-11:50AM'), + ('T_12:00-1:50PM', 'T 12:00-1:50PM'), + ('T_2:00-3:50PM', 'T 2:00-3:50PM'), + ('T_4:00-4:50PM', 'T 4:00-4:50PM'), + ('T_5:00-5:50PM', 'T 5:00-5:50PM'), + ('T_6:00-6:50PM', 'T 6:00-6:50PM'), + ('W_10:00-10:50AM', 'W 10:00-10:50AM'), + ('W_10:00-11:50AM', 'W 10:00-11:50AM'), + ('W_11:00-11:50AM', 'W 11:00-11:50AM'), + ('W_12:00-12:50PM', 'W 12:00-12:50PM'), + ('W_12:00-1:50PM', 'W 12:00-1:50PM'), + ('W_1:00-1:50PM', 'W 1:00-1:50PM'), + ('W_2:00-3:50PM', 'W 2:00-3:50PM'), + ('W_4:00-4:50PM', 'W 4:00-4:50PM'), + ('W_4:00-5:50PM', 'W 4:00-5:50PM'), + ('W_6:00-7:50PM', 'W 6:00-7:50PM'), + ('R_4:00-5:50PM', 'R 4:00-5:50PM'), + ('R_6:00-7:50PM', 'R 6:00-7:50PM'), + ) + self.fields["time_slots"] = forms.MultipleChoiceField( + choices=time_slots_choices, + label=False, + required=False, + widget=forms.CheckboxSelectMultiple() + ) + + + self.helper.layout = Layout( + HTML('''

SCHEDULING

'''), + HTML("""
"""), + HTML("
"), + HTML('''
Indicate your availability for the + Spring 2020 semester by carefully checking all boxes that apply. The more boxes you check, + the more likely you will be a mentor. Note that there is overlap in some of the days/times listed below.
'''), + Div("time_slots"), + HTML("""
"""), + HTML('''
Please list other days/times of additional availability. The more days/times you are + available, the more likely you will be selected as a mentor. Do not leave this blank.
'''), + Field('other_times', style = "width:100%"), + HTML("
"), + ) + self.helper.form_method = 'POST' + self.helper.form_show_labels = False + self.helper.add_input(Button('prev', 'Prev', onclick="window.history.go(-1); return false;")) + self.helper.add_input(Submit('next', 'Next', onclick="VoteUtil.submitPref();")) + + + +class MentorApplicationfoForm_step6(ModelForm): + class Meta: + model = Mentor + fields = ( 'relevant_info', ) + widgets = { + 'relevant_info': forms.Textarea(attrs={'cols': 100, 'rows': 8}) + } + + def __init__(self, *args, **kwargs): + super(MentorApplicationfoForm_step6, self).__init__(*args, **kwargs) + self.helper = FormHelper() + + self.fields["relevant_info"] = forms.CharField(widget=forms.Textarea(attrs={'cols': 100, 'rows': 8})) + self.helper.layout = Layout( + HTML('''

ADDITIONAL INFOMATION

'''), + HTML("""
"""), + HTML("
"), + HTML('''
Please provide any other relevant information about yourself in the space below, including your mentoring preferences (i.e., which courses you'd prefer to mentor, which courses you'd prefer not to mentor, other extracurricular CS activities you're involved with, etc.). Do not leave this blank.
'''), + Field('relevant_info', style = "width:100%"), + HTML("""
"""), + + HTML('''
After you choose to submit, you can save your profile and change it if needed at any time after.
'''), + + HTML("
"), + ) + self.helper.form_method = 'POST' + self.helper.form_show_labels = False + self.helper.add_input(Button('prev', 'Prev', onclick="window.history.go(-1); return false;")) + self.helper.add_input(Submit("save and submit", "Save and Submit")) + +''' +# DEPRECATED +class MentorApplicationfoForm(ModelForm): + class Meta: + model = Mentor + #fields = '__all__' + fields = ( 'RIN', + 'first_name', + 'last_name', + 'GPA', + 'email', + 'phone', + 'recommender', + 'compensation', + ) + + help_texts = { + 'RIN': _(' *Required'), + 'first_name': _(' *Required'), + 'last_name': _(' *Required'), + 'GPA': _(' *Required'), + 'email': _(' *Required'), + 'phone': _(' *Required'), + 'recommender': _(' *Required'), + } + + widgets = { + 'RIN': forms.TextInput(attrs={'placeholder': ' 661680100'}), + 'GPA': forms.TextInput(attrs={'placeholder': ' 3.6'}), + 'email': forms.TextInput(attrs={'placeholder': ' xxx@rpi.email'}), + 'phone': forms.TextInput(attrs={'placeholder': ' 5185941234'}), + } + def __init__(self, *args, **kwargs): + super(MentorApplicationfoForm, self).__init__(*args, **kwargs) + #self.fields["course_pref"].required = False + + self.helper = FormHelper() + self.helper.form_id = 'id-exampleForm' + self.helper.form_class = 'blueForms' + self.helper.form_method = 'post' + #self.helper.form_action = 'applyfunc1' + courses = Course.objects.all() + course_layout = Div() + + # Add courses fields + for course in courses: + course_layout.append(HTML("
")) + course_layout.append( HTML("
" + course.subject +" "+ course.number +" "+ course.name + "
") ) + grades = ( + ('a', 'A'), + ('a-', 'A-'), + ('b+', 'B+'), + ('b', 'B'), + ('b-', 'B-'), + ('c+', 'C+'), + ('c', 'C'), + ('c-', 'C-'), + ('d+', 'D+'), + ('d', 'D'), + ('f', 'F'), + ('p', 'Progressing'), + ('n', 'Not Taken'), + ) + choices_YN = ( + ('Y', 'YES'), + ('N', 'NO'), + ) + self.fields[course.name+ "_grade"] = forms.ChoiceField( + choices = grades, + label = "Grade on this course:", + ) + #self.initial[course.name+ "_grade"] = 'n' + + self.fields[course.name+ "_exp"] = forms.ChoiceField(choices = choices_YN, label = 'Have you mentored this class before? ') + #self.initial[course.name+ "_exp"] = 'N' + + course_layout.append(Field(course.name+ "_grade", label_class = "long_label")) + #course_layout.append(HTML('

')) + course_layout.append(InlineRadios(course.name+ "_exp")) + course_layout.append(HTML("
")) + + # Time slot choices + SCHEDULING = ( + ('M1', 'YES'), + ('M2', 'NO'), + ) + self.helper.layout = Layout( + Accordion( + AccordionGroup('== PERSONAL INFORMATION ==', + HTML("
"), + Field('RIN', 'first_name', 'last_name', 'email', 'phone', 'GPA', css_class = ""), + HTML(""" +
Please provide the name of someone in the CS Department who can recommend you. You are encouraged, but not required, to contact this person. +
"""), + HTML(""" +
Please provide only a name here (describe additional circumstances in the freeform textbox below).
+ """), + Field('recommender', css_class = "inputline"), + HTML("
"), + ), + + AccordionGroup('== COMPENSATION AND RESPONSIBILITIES ==', + HTML("
"), + HTML(""" +
+ We currently have funding for a fixed number of paid programming mentors. Therefore, if you apply for pay, you may be asked to work for credit instead. Course credit counts as a graded 1-credit or 2-credit free elective. In general, if you work an average of 1-3 hours per week, you will receive 1 credit; if you average 4 or more hours per week, you will receive 2 credits. Note that no more than 4 credits may be earned as an undergraduate (spanning all courses and all semesters). Please do not apply for credit if you have already earned 4 credits as a mentor; apply only for pay. +
+ """), + Field('compensation', css_class = ""), + HTML(""" +
+ For a paid position, you must follow + the given instructions to ensure you are paid; otherwise,  + another candidate will be selected.  More specifically, you  + will be required to have a student employment card. + This requires you to have a federal I-9 on file.  Bring appropriate  + identification with you to Amos Eaton 109 if this is your first  + time working for RPI; +
+ """), + HTML("
"), + ), + + AccordionGroup('== COURSE SELECTIONS ==', + HTML("
"), + course_layout, + HTML("
"), + ), + + AccordionGroup('== COURSE RANKINGS ==', + HTML("
"), + + HTML(''Please rank the courses which you prefer to mentor, #1 means the highest prioity, and #2 means the second priority...etc. This will help us to allocate your position.''), + + HTML('' + ''), + HTML('' + + + + ''), + HTML("
"), + ), + + AccordionGroup('== SCHEDULING ==', + HTML("
"), + HTML("Time slot plugin here"), + HTML("
"), + ), + ) + ) + self.helper.form_method = 'POST' + self.helper.label_class = "my_label" + self.helper.add_input(Submit('submit', 'Submit', onclick="VoteUtil.submitPref();")) + + # Overide the save func + def save(self, *args, **kwargs): + new_applicant = Mentor() + new_applicant.RIN = self.cleaned_data["RIN"] + new_applicant.first_name = self.cleaned_data["first_name"] + new_applicant.last_name = self.cleaned_data["last_name"] + new_applicant.GPA = self.cleaned_data["GPA"] + new_applicant.phone = self.cleaned_data["phone"] + new_applicant.compensation = self.cleaned_data["compensation"] + + # create a dictionary to store a list of preference + pref = Dict() + pref.name = new_applicant.RIN + pref.save() + + new_applicant.course_pref = pref + new_applicant.save() + #orderStr = self.cleaned_data["pref_order"] + + # Save Grades on the course average + for course in Course.objects.all(): + course_grade = course.name + "_grade" + course_exp = course.name + "_exp" + + new_grade = Grade() + new_grade.student = new_applicant + new_grade.course = course + new_grade.student_grade = self.cleaned_data[course_grade] # Grade on this course + if (self.cleaned_data[course_exp] == 'Y'): # Mentor Experience + new_grade.mentor_exp = True + else: + new_grade.mentor_exp = False + + # if the student has failed the class, or is progressing, we consider he did not take the course + if (new_grade.student_grade != 'p' and new_grade.student_grade != 'n' and new_grade.student_grade != 'f'): + new_grade.have_taken = True + else: + new_grade.have_taken = False + new_grade.save() + print(new_grade.course.name + ": " + new_grade.student_grade.upper()) + + return new_applicant + +''' \ No newline at end of file diff --git a/compsocsite/mentors/match.py b/compsocsite/mentors/match.py new file mode 100755 index 00000000..f23f595b --- /dev/null +++ b/compsocsite/mentors/match.py @@ -0,0 +1,339 @@ +from collections import defaultdict, deque + +class Matcher: + + #constructs a Matcher instance + + #studentPrefs is a dict from student to class ranking as a list + #e.g. "student1" -> ["class1", "class3", "class2"] + #the ranking doesn't need to be complete + #all missing classes are treated as "worse than nothing" + + #studentFeatures is a dict from student to dict(class -> feature vector) + #e.g "student1" -> {"class1" -> (0, 1, 3.8), "class2" -> (1, 2, 3.8)} + #that is, studentFeatures[s][c] is student s's feature vector in terms of class c + #each student needs to have a feature vector for each class in their ranking + + #classCaps is a dict from class to maximum capacity + #e.g. "class1" -> 5 + + #classFeatures is a dict from class to feature vector + #e.g. "class1" -> (98, 70, 65) + + #the dot product of a student's feature vector with a particuar class's feature vector is its score + #in terms of that class + def __init__(self, studentPrefs, studentFeatures, classCaps, classFeatures): + + self.studentPrefs = studentPrefs + self.classCaps = classCaps + + + #a student may not have a feature vector for every class + #in that case, we convert all the inner dict(class -> vector) objects to defaultdicts + #that way, if we make a call studentFeatures[s][c], but student s doesn't have a feature vector for class c, + #instead of getting a ValueError, we get an empty tuple () + #doing a dot product with an empty tuple will give a score of 0, which is what we want + studentFeatures = {s: defaultdict(tuple, d) for s, d in studentFeatures.items()} + + #dot product of a and b + #only does dot over the first min(len(a), len(b)) elements + def dot(a, b): + #it's ok if lengths aren't equal, will just take shortest length, + #so we don't really need this assert + #assert len(a) == len(b) + return sum(x*y for x, y in zip(a, b)) + + #returns a complete ranking over students for one specific class c + def makeClassPref(c): + + #return list of students, sorted by dot product, then student name (for tiebreaking) + #if a student doesn't have a feature vector for this class, we will get (), which will make their score 0 + return sorted(studentPrefs.keys(), key=lambda s: (dot(classFeatures[c], studentFeatures[s][c]), s), reverse=True) + + #call makeClassPref on each class to make the preferences + self.classPrefs = {c: makeClassPref(c) for c in classFeatures.keys()} + + + #output variables + self.studentMatching = {} #student -> class + self.classMatching = defaultdict(list) #class -> [students] + + #we index preferences at initialization to avoid expensive lookups when matching + self.classRank = defaultdict(dict) #classRank[c][s] is c's ranking of s + self.studentRank = defaultdict(dict) #studentRank[s][c] is s's ranking of c + + #if ranking isn't present, treated as less than none + + for c, prefs in self.classPrefs.items(): + for i, s in enumerate(prefs): + self.classRank[c][s] = i + + for s, prefs in studentPrefs.items(): + for i, c in enumerate(prefs): + self.studentRank[s][c] = i + + + + #Test whether s prefers c over c2. + def prefers(self, s, c, c2): + ranking = self.studentRank[s] + + if c in ranking: + + if c2 in ranking: + #both in ranking + + #return normally + return ranking[c] < ranking[c2] + + else: + #c in ranking, but c2 not in ranking + + #c is preferred + return True + + else: + if c2 in ranking: + #c not in ranking, but c2 in ranking + + #c2 is preferred + return False + + else: + #both not in ranking + + #none, none: false + #strr, none: false + #none, strr: true + #strr, strr: c < c2 + + #None represents no one. it can be thought of as being after the last ranked element, + #and before the unranked ones + + if c2 == None: + #either c is str and c2 is none, or both none + #either way, c is not preferred over c2 + return False + + if c == None: + #c is none and c2 is str + #so c, being no one, is preferred over unranked c2 + return True + + + #if we get here, none isn't involved + #both c and c2 are unranked str's + + #preference based on alphabetical order + return c < c2 + + + #Return the student favored by c after s. + def after(self, c, s): + + #TODO extra checking here might not be needed as classes have full ranking + + if s not in self.classRank[c]: + #TODO will this happen? probably not + print(f"student {s} is not in {c}'s ranking") + #assert False + + #in case it does happen, + #we return the next alphabetical student who is also not ranked + #it is expensive though + + students = sorted(self.studentPrefs.keys()) + + #if we're trying to find the next student after "no one", + #we search from the beginning + if s == None: + i = 0 + + else: + i = students.index(s)+1 + + while i < len(students) and students[i] in self.classRank[c]: + i += 1 + + + if i >= len(students): + #this alg in't perfect. we're mixing what None means + #none usually means "no one" + #but here it means there isn't a next student + #this probably doesn't matter much, as this code will probably not be used + return None + + print(f"using {students[i]}") + return students[i] + + #index of student following s in list of prefs + i = self.classRank[c][s] + 1 + + if i >= len(self.classPrefs[c]): + #no other students are prefered. + return None + + return self.classPrefs[c][i] + + + + #Try to match all classes with their next preferred spouse. + #does class-proposing Gale-Shapely + def match(self): + + #simple combination of a queue with a set + #the set is used for quick "contains" lookups + class SmartQueue: + + def __init__(self): + self.queue = deque() + self.set = set() + + def put(self, x): + self.queue.append(x) + self.set.add(x) + + def pop(self): + x = self.queue.popleft() + self.set.remove(x) + return x + + def __contains__(self, x): + return x in self.set + + def __len__(self): + return len(self.queue) + + + #classes (full ranking) + #students (partial ranking) + + #queue of classes we still have to match + queue = SmartQueue() + + #next is a map from class to next student to propose to + #starts as first preferences + next = {} + + #mapping from students to current class + studentMatching = {} + + #the current capacity for each class + currCap = defaultdict(int) + + #initalize + #we do this in a loop so we only iterate over self.classPrefs once + for c, rank in self.classPrefs.items(): + #we start with all the classes in the queue + queue.put(c) + + #and all the classes will propose to their top ranking student + next[c] = rank[0] + + + #while we still have classes to match + while len(queue) > 0: + + #take class off list + c = queue.pop() + + #make proposals for the remaining capacity + for i in range(self.classCaps[c] - currCap[c]): + + #next student for c to propose to + s = next[c] + + #if we run out of students to propose to + if s == None: + #we break, finished matching, but having less than max capacity + break + + #student after s in c's list of prefs + #"next-next" student for c to propose to + next[c] = self.after(c, s) + + + #the class s is currently matched to, or None if unmatched + c2 = None + + if s in studentMatching: + c2 = studentMatching[s] + + assert c2 != c + + + #we propose to c + #if s prefers c more than the current class / being unmatched + if self.prefers(s, c, c2): + + #we will move s from c2 to c + #we don't need to do this if c2 was none, + #meaning s was unmatched + if c2 != None: + + #unmatch to old class + currCap[c2] -= 1 + + #if c2 isn't already scheduled to match, put it in the queue + if c2 not in queue: + queue.put(c2) + + + #s becomes matched to c + studentMatching[s] = c + currCap[c] += 1 + + + #otherwise, we're rejected + #just go to the next proposal + + + #we finished the proposals for this class for this round + #now, does this class need another round? + + #if we aren't full, and haven't been "none'd", we re-add ourself + #"none'd" meaning we ran out of students to propose to + if currCap[c] < self.classCaps[c] and next[c] != None: + queue.put(c) + + + #now we've matched all classes, so we're done + + #populate studentMatching + self.studentMatching = studentMatching + + #populate classMatching from studentMatching + for s, c in studentMatching.items(): + self.classMatching[c].append(s) + + + return self.classMatching + + #check if the mapping of studentMatching to husbands is stable + #TODO this doesn't look at unmatched students or classes. is that a problem? + def isStable(self, studentMatching=None, verbose=False): + + if studentMatching is None: + studentMatching = self.studentMatching + + for s, c in studentMatching.items(): + + i = self.classRank[c][s] + + preferred = self.classPrefs[c][:i] + + for p in preferred: + + #it's possible p is unmatched + #in that case, c2 is None + c2 = None + if p in studentMatching: + c2 = studentMatching[p] + + #check if p prefers us over current matching + #if c2 is none, this just checks if p prefers us to nobody + if self.prefers(p, c, c2): + if verbose: + print(f"{c}'s marriage to {s} is unstable:\n{c} prefers {p} over {s} and {p} prefers {c} over her current husband {c2}") + return False + return True diff --git a/compsocsite/mentors/migrations/0001_initial.py b/compsocsite/mentors/migrations/0001_initial.py new file mode 100644 index 00000000..c0cfc664 --- /dev/null +++ b/compsocsite/mentors/migrations/0001_initial.py @@ -0,0 +1,92 @@ +# Generated by Django 2.2 on 2019-11-25 06:56 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import mentors.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Course', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject', models.CharField(max_length=4)), + ('number', models.CharField(default='1000', max_length=4)), + ('name', models.CharField(default='none', max_length=50)), + ('instructor', models.CharField(default='none', max_length=50)), + ('time_slots', models.CharField(default='[]', max_length=500)), + ('feature_cumlative_GPA', models.IntegerField(default=0)), + ('feature_has_taken', models.IntegerField(default=0)), + ('feature_course_GPA', models.IntegerField(default=0)), + ('feature_mentor_exp', models.IntegerField(default=0)), + ('mentor_cap', models.IntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='Dict', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=1000)), + ], + ), + migrations.CreateModel( + name='Instrcutor', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=50)), + ('last_name', models.CharField(max_length=50)), + ('department', models.CharField(max_length=50)), + ], + ), + migrations.CreateModel( + name='Mentor', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('applied', models.BooleanField(default=False)), + ('RIN', models.CharField(max_length=9, validators=[django.core.validators.MinLengthValidator(9)])), + ('first_name', models.CharField(default='', max_length=50)), + ('last_name', models.CharField(default='', max_length=50)), + ('GPA', mentors.models.MinMaxFloat(default=0)), + ('email', models.CharField(max_length=50)), + ('phone', models.CharField(default='', max_length=10)), + ('recommender', models.CharField(default='', max_length=50)), + ('compensation', models.CharField(choices=[('n', 'No Preference'), ('p', 'Pay ($14/hour) '), ('c', 'Credit')], default='n', max_length=1)), + ('studnet_status', models.CharField(choices=[('i', 'International'), ('d', 'Domestic')], default='i', max_length=1)), + ('employed_paid_before', models.BooleanField(default=False)), + ('mentored_non_cs_bf', models.BooleanField(default=False)), + ('time_slots', models.CharField(default='[]', max_length=1000)), + ('other_times', models.CharField(default='', max_length=1000)), + ('relevant_info', models.CharField(default='', max_length=1000)), + ('course_pref', models.CharField(max_length=10000)), + ('mentored_course', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='mentors.Course')), + ], + ), + migrations.CreateModel( + name='KeyValuePair', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(db_index=True, max_length=240)), + ('value', models.CharField(db_index=True, max_length=240)), + ('container', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mentors.Dict')), + ], + ), + migrations.CreateModel( + name='Grade', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('student_grade', models.CharField(choices=[('a', 'A'), ('a-', 'A-'), ('b+', 'B+'), ('b', 'B'), ('b-', 'B-'), ('c+', 'C+'), ('c', 'C'), ('c-', 'C-'), ('d+', 'D+'), ('d', 'D'), ('f', 'F'), ('p', 'Progressing'), ('n', 'Not Taken')], default='n', max_length=1)), + ('have_taken', models.BooleanField(default=False)), + ('mentor_exp', models.BooleanField(default=False)), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mentors.Course')), + ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mentors.Mentor')), + ], + ), + ] diff --git a/compsocsite/mentors/migrations/__init__.py b/compsocsite/mentors/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/compsocsite/mentors/models.py b/compsocsite/mentors/models.py new file mode 100644 index 00000000..93a93c5e --- /dev/null +++ b/compsocsite/mentors/models.py @@ -0,0 +1,313 @@ +from __future__ import unicode_literals + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.contrib.auth.models import User +from django.core.validators import MinLengthValidator + +@python_2_unicode_compatible + +# a helper function to ensure min and max value +class MinMaxFloat(models.FloatField): + def __init__(self, min_value=None, max_value=None, *args, **kwargs): + self.min_value, self.max_value = min_value, max_value + super(MinMaxFloat, self).__init__(*args, **kwargs) + + def formfield(self, **kwargs): + defaults = {'min_value': self.min_value, 'max_value' : self.max_value} + defaults.update(kwargs) + return super(MinMaxFloat, self).formfield(**defaults) + + + +# https://djangosnippets.org/snippets/2451/ +# unmodified version of Dict +class Dict(models.Model): + """A model that represents a Dict. This model implements most of the Dict interface, + allowing it to be used like a python Dict. + + """ + name = models.CharField(max_length = 1000) + + @staticmethod + def getDict(name): + """Get the Dict of the given name. + + """ + df = Dict.objects.select_related().get(name=name) + + return df + + def __getitem__(self, key): + """Returns the value of the selected key. + + """ + return self.keyvaluepair_set.get(key=key).value + + def __setitem__(self, key, value): + """Sets the value of the given key in the Dict. + + """ + try: + kvp = self.keyvaluepair_set.get(key=key) + + except KeyValuePair.DoesNotExist: + KeyValuePair.objects.create(container=self, key=key, value=value) + + else: + kvp.value = value + kvp.save() + + def __delitem__(self, key): + """Removed the given key from the Dict. + + """ + try: + kvp = self.keyvaluepair_set.get(key=key) + + except KeyValuePair.DoesNotExist: + raise KeyError + + else: + kvp.delete() + + def __len__(self): + """Returns the length of this Dict. + + """ + return self.keyvaluepair_set.count() + + def iterkeys(self): + """Returns an iterator for the keys of this Dict. + + """ + return iter(kvp.key for kvp in self.keyvaluepair_set.all()) + + def itervalues(self): + """Returns an iterator for the keys of this Dict. + + """ + return iter(kvp.value for kvp in self.keyvaluepair_set.all()) + + __iter__ = iterkeys + + def iteritems(self): + """Returns an iterator over the tuples of this Dict. + + """ + return iter((kvp.key, kvp.value) for kvp in self.keyvaluepair_set.all()) + + def keys(self): + """Returns all keys in this Dict as a list. + + """ + return [kvp.key for kvp in self.keyvaluepair_set.all()] + + def values(self): + """Returns all values in this Dict as a list. + + """ + return [kvp.value for kvp in self.keyvaluepair_set.all()] + + def items(self): + """Get a list of tuples of key, value for the items in this Dict. + This is modeled after dict.items(). + + """ + return [(kvp.key, kvp.value) for kvp in self.keyvaluepair_set.all()] + + def get(self, key, default=None): + """Gets the given key from the Dict. If the key does not exist, it + returns default. + + """ + try: + return self[key] + + except KeyError: + return default + + def has_key(self, key): + """Returns true if the Dict has the given key, false if not. + + """ + return self.contains(key) + + def contains(self, key): + """Returns true if the Dict has the given key, false if not. + + """ + try: + self.keyvaluepair_set.get(key=key) + return True + + except KeyValuePair.DoesNotExist: + return False + + def clear(self): + """Deletes all keys in the Dict. + + """ + self.keyvaluepair_set.all().delete() + + def __unicode__(self): + """Returns a unicode representation of the Dict. + + """ + return unicode(self.asPyDict()) + + def asPyDict(self): + """Get a python Dict that represents this Dict object. + This object is read-only. + + """ + fieldDict = dict() + + for kvp in self.keyvaluepair_set.all(): + fieldDict[kvp.key] = kvp.value + + return fieldDict + + +class KeyValuePair(models.Model): + """A Key-Value pair with a pointer to the Dict that owns it. + + """ + container = models.ForeignKey(Dict, db_index=True, on_delete = models.CASCADE) + key = models.CharField(max_length=240, db_index=True) + value = models.CharField(max_length=240, db_index=True) + + + +# The model for a course +class Course(models.Model): + subject = models.CharField(max_length=4) # e.g CSCI + number = models.CharField(max_length=4, default="1000") # e.g 1100 + name = models.CharField(max_length=50, default = 'none') # e.g Intro to programming + + # This should change to foreignkey afterwards + # But we left this now to make the implementation easy + instructor = models.CharField(max_length=50, default = 'none') # Instrcutor's name + time_slots = models.CharField(max_length=500, default = '[]') # Instrcutor's name + + # feature weights to represent the pref + feature_cumlative_GPA = models.IntegerField(default=0) + feature_has_taken = models.IntegerField(default=0) + feature_course_GPA = models.IntegerField(default=0) + feature_mentor_exp = models.IntegerField(default=0) + + # mentor capacity each course to have + mentor_cap = models.IntegerField(default = 0) + + def __str__(self): + return self.subject + " " + self.number + " " + self.name + +# the model to represent a mentor applicant +class Mentor(models.Model): + + # already applied for this semester + applied = models.BooleanField(default = False) + #step = models.IntegerField(default = 1) + + # Personal Info & preference of applicants + #RIN = models.CharField(max_length=9, validators=[MinLengthValidator(9)], primary_key=True) + + RIN = models.CharField(max_length=9, validators=[MinLengthValidator(9)]) + first_name = models.CharField(max_length=50, default="") # first name + last_name = models.CharField(max_length=50, default="") # last name + GPA = MinMaxFloat(min_value = 0.0, max_value = 4.0, default=0) + email = models.CharField(max_length=50) + phone = models.CharField(max_length=10, default="") # ??? + recommender = models.CharField(max_length=50, default="") + + # Compensation Choices + compensation_choice = ( + ('n', 'No Preference'), + ('p', 'Pay ($14/hour) '), + ('c', 'Credit'), + ) + compensation = models.CharField(max_length=1, choices = compensation_choice, default='n') + status = ( + ('i', 'International'), + ('d', 'Domestic'), + ) + studnet_status = models.CharField(max_length=1, choices = status, default='i') + employed_paid_before = models.BooleanField(default = False) + mentored_non_cs_bf = models.BooleanField(default = False) + ''' + time_slots_choices = ( + ('M_4:00-4:50PM', 'M 4:00-4:50PM'), + ('M_4:00-5:50PM', 'M 4:00-5:50PM'), + ('M_5:00-5:50PM', 'M 5:00-5:50PM'), + ('M_6:00-6:50PM', 'M 6:00-6:50PM'), + ('T_10:00-11:50AM', 'T 10:00-11:50AM'), + ('T_12:00-1:50PM', 'T 12:00-1:50PM'), + ('T_2:00-3:50PM', 'T 2:00-3:50PM'), + ('T_4:00-4:50PM', 'T 4:00-4:50PM'), + ('T_5:00-5:50AM', 'T 5:00-5:50AM'), + ('T_6:00-6:50PM', 'T 6:00-6:50PM'), + ('W_10:00-11:50AM', 'W 10:00-11:50AM'), + ('W_12:00-1:50PM', 'W 12:00-1:50PM'), + ('W_2:00-3:50PM', 'W 2:00-3:50PM'), + ('W_4:00-4:50PM', 'W 4:00-4:50PM'), + ('W_5:00-5:50AM', 'W 5:00-5:50AM'), + ('W_6:00-6:50PM', ' W 6:00-7:50PM'), + ('T_4:00-5:50AM', 'T 4:00-5:50AM'), + ('T_6:00-6:50PM', 'T 6:00-7:50PM'), + ) + time_slots = models.CharField(choices = time_slots_choices, max_length = 20, default= "") + ''' + time_slots = models.CharField(max_length = 1000, default= "[]") + + other_times = models.CharField(max_length = 1000, default = "") + relevant_info = models.CharField(max_length = 1000, default = "") + + + # Course preference of applicants, the data model here is dictionary + # Yeah we can do charfield... will change it afterwards + #course_pref = models.ForeignKey(Dict, on_delete = models.CASCADE, default = None) + course_pref = models.CharField(max_length=10000) + # Many to one relation + # mentor_course -> {s1, s2, s3, ...} + # To get all the mentors in a course: course.mentor_set.all() + mentored_course = models.ForeignKey(Course, on_delete = models.CASCADE, default = None, null=True) + + + def __str__(self): + return self.first_name + " " + self.last_name + + + +class Grade(models.Model): + student = models.ForeignKey(Mentor, on_delete=models.CASCADE) + course = models.ForeignKey(Course, on_delete=models.CASCADE) + + grades = ( + ('a', 'A'), + ('a-', 'A-'), + ('b+', 'B+'), + ('b', 'B'), + ('b-', 'B-'), + ('c+', 'C+'), + ('c', 'C'), + ('c-', 'C-'), + ('d+', 'D+'), + ('d', 'D'), + ('f', 'F'), + ('p', 'Progressing'), + ('n', 'Not Taken'), + ) + + student_grade = models.CharField(max_length=1, choices = grades, default='n') # The student's grade of this course + have_taken = models.BooleanField(default = False) # Whether this studnet have taken this course + mentor_exp = models.BooleanField(default = False) # Whether this studnet have mentored this course + + + +class Instrcutor(models.Model): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + department = models.CharField(max_length=50) + + def __str__(self): + return self.first_name + " " + self.last_name \ No newline at end of file diff --git a/compsocsite/mentors/templates/mentors/apply.html b/compsocsite/mentors/templates/mentors/apply.html new file mode 100755 index 00000000..8eab6c85 --- /dev/null +++ b/compsocsite/mentors/templates/mentors/apply.html @@ -0,0 +1,38 @@ +{% extends 'polls/base.html' %} + +{% block content %} +{% if user.is_authenticated %} + + +
+

+ {% if messages %} +

+ {% endif %} +

+
+{% if not applied %} +
+
+
+ Mentor application Form +
+ +
+ {% load crispy_forms_tags %} + {% crispy apply_form apply_form.helper %} + {{ form.errors }} +
+
+
+{% else %} +
Please Login
+{% endif %} +{% endif %} +{% endblock %} diff --git a/compsocsite/mentors/templates/mentors/course_feature.html b/compsocsite/mentors/templates/mentors/course_feature.html new file mode 100755 index 00000000..bc979ce6 --- /dev/null +++ b/compsocsite/mentors/templates/mentors/course_feature.html @@ -0,0 +1,74 @@ +{% extends 'mentors/apply.html' %} +{% load staticfiles %} +{% block content %} + + + +
+
+
+
+ +
+ {% csrf_token %} + Choose Course: + + + +
+ {% if choosen_course %} +
+ {% csrf_token %} +
{{choosen_course.name}}:
+ +
Time Slots:
+ + {% for time in time_slots.times%} +
{{time}}
+ {% endfor %} +
Capacity: {{choosen_course.mentor_cap}}
+
+
    +
  • + Cumlative GPA :
    {{choosen_course.feature_cumlative_GPA}}
    +
    +
  • +
  • + Grade on this course before :
    {{choosen_course.feature_course_GPA}}
    +
    +
  • +
  • + Have taken the course :
    {{choosen_course.feature_has_taken}}
    +
    +
  • +
  • + Have Mentor Experience :
    {{choosen_course.feature_mentor_exp}}
    +
    +
  • +
+
+ + + + + + +
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/compsocsite/mentors/templates/mentors/index.html b/compsocsite/mentors/templates/mentors/index.html new file mode 100755 index 00000000..512ed7f7 --- /dev/null +++ b/compsocsite/mentors/templates/mentors/index.html @@ -0,0 +1,176 @@ +{% extends 'polls/base.html' %} + +{% block content %} +{% if user.is_authenticated %} +
+

+ {% if messages %} +

+ {% endif %} +

+
+ + + +
+
+
+ Spring 2020 Undergraduate Programming Mentor Application Form +
+ +
+

+ The Computer Science Department is currently recruiting undergraduate + programming mentors for Spring 2020 courses! +

+ +

+ Undergraduate programming mentors help students better understand computer science and, + in particular, programming. + Mentoring typically occurs during labs and recitations, with either the instructor or + TA heading each session. + Mentors also typically hold group office hours. + And for some courses, mentors meet periodically + with TAs and instructors to receive guidance as to how to best mentor. +

+ +

+ Because mentoring for CSCI requires approximately six hours per week + on top of your other commitments, + please do not mentor for other departments + (e.g., the I-PERSIST mentoring program). +

+ +

+ Please apply below. + Applications are due by 12/6/2019. + Send any questions + to Shianne Hulbert. + And please be sure to specify your schedule below to the best of your knowledge. +

+
+ {% if applied %} + +

Your mentor application is successful!

+

You can change your application at anytime, but remember, we will start to look at all the applications after 12/6, so make sure you filled out the form and submit before the deadline.

+ + View your application and make changes:   + View and Change + + {% if dev %} + View + {% endif %} +
+ +

+ You can withdraw your application if you do not want to apply for mentor anymore. A new application can be started after you withdrawed.

+ Withdraw apllication +

+ + + {% else %} + Begin to apply:   + Apply + {% endif %} +
+
+
+ + +{% if admin %} + + +
+
+
+ Professor Interface +
+ +
+ Check your courses:   + View + +
+
+
+ + +
+
+
+ Administrator Page +
+
+
+ {% csrf_token %} +

You can upload csv and generate new courses.

+

For the old courses, you can change mentor capacity and time slots(['t1', 't2'...]).

+

But you can not delete them right now for the sake of secruity

+ Load courses from upload:   +
+ + + +
+
+ +
+ {% csrf_token %} + Download the csv of all applicants:   + +
+
+ + {% csrf_token %} + View all the applicants:   + View +
+ + {% if dev %} +
+ {% csrf_token %} + Add Student Random (DEV): + + +
+ +
+ {% csrf_token %} + Match:   + +
+
+ View Match Result + {% endif %} + +
+
+
+{% endif %} +{% endif %} +{% endblock %} + diff --git a/compsocsite/mentors/templates/mentors/view_application.html b/compsocsite/mentors/templates/mentors/view_application.html new file mode 100755 index 00000000..3e2ea0c7 --- /dev/null +++ b/compsocsite/mentors/templates/mentors/view_application.html @@ -0,0 +1,73 @@ +{% extends 'polls/base.html' %} + +{% block content %} +{% if user.is_authenticated %} +
+

+ {% if messages %} +

+ {% endif %} +

+
+
+
+
+ View Your Application +
+
+
+ {% for application in applications %} +
+ {{ application.RIN }} +
+
+ {{ application.first_name }} {{ application.last_name }} + +
+
+ {{ application.RPI_email }} +
+
+ {{ application.phone }} +
+
+ {{ application.GPA }} +
+
+ {{ application.recommender }} +
+ {% endfor %} + +
+
+
+
+
+ Groups you can join +
+
+ + {% if opengroups %} + + {% for group in opengroups %} + {% if request.user != group.owner and request.user not in group.members.all %} + + + + + {% endif %} + {% endfor %} + + {% endif %} +
{{ group.name }}Join
+
+
+
+{% endif %} +{% endblock %} diff --git a/compsocsite/mentors/templates/mentors/view_match_result.html b/compsocsite/mentors/templates/mentors/view_match_result.html new file mode 100755 index 00000000..8dfd7cca --- /dev/null +++ b/compsocsite/mentors/templates/mentors/view_match_result.html @@ -0,0 +1,35 @@ +{% extends 'polls/base.html' %} + +{% block content %} +{% if user.is_authenticated %} +{% if isAdmin %} +
+

+ {% if messages %} +

+ {% endif %} +

+
+ +
+
+
+ {% for course in result.courses|dictsort:"number"%} +
{{course.name}}  [features: {{course.features}}]
+ {% for mentor in course.mentors %} +
  {{mentor.name}}, GPA: {{mentor.GPA}}, grade: {{mentor.grade}}, mentor experience: {{mentor.Exp}}
+ {% endfor %} +

+ {% endfor %} +
+
+
+{% endif %} +{% endif %} +{% endblock %} diff --git a/compsocsite/mentors/templates/mentors/view_students.html b/compsocsite/mentors/templates/mentors/view_students.html new file mode 100755 index 00000000..c3223d86 --- /dev/null +++ b/compsocsite/mentors/templates/mentors/view_students.html @@ -0,0 +1,26 @@ +{% extends 'polls/base.html' %} + +{% block content %} +{% if user.is_authenticated %} +{% if isAdmin %} +
+
+
+ Applicants +
+
+
+ Number of students applied:   {{ applicants_number }} + {% for applicant in applicants %} +
+ {{forloop.counter}}.  RIN: {{ applicant.RIN }}   {{ applicant }}   {{ applicant.email }}  {{ applicant.phone }}   GPA: {{ applicant.GPA }}   Recommender: {{ applicant.recommender }} +
+ {% endfor %} + +
+
+
+
+{% endif %} +{% endif %} +{% endblock %} diff --git a/compsocsite/mentors/tests.py b/compsocsite/mentors/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/compsocsite/mentors/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/compsocsite/mentors/urls.py b/compsocsite/mentors/urls.py new file mode 100644 index 00000000..2948858c --- /dev/null +++ b/compsocsite/mentors/urls.py @@ -0,0 +1,39 @@ +from django.conf.urls import url +from django.contrib.auth.decorators import login_required +from django.views.decorators.cache import cache_page +from . import views +from django.urls import path + + + +app_name = 'mentors' +urlpatterns = [ + url(r'^$', login_required(views.viewindex), name='index'), + #url(r'^apply$', login_required(views.ApplyView.as_view()), name='apply'), + + # Mentor applciation steps 1-6 + url(r'^apply/$', login_required(views.applystep), name='applyfunc1'), + url(r'^applystep2/$', login_required(views.applystep2), name='applystep2'), + url(r'^applystep3/$', login_required(views.applystep3), name='applystep3'), + url(r'^applystep4/$', login_required(views.applystep4), name='applystep4'), + url(r'^applystep5/$', login_required(views.applystep5), name='applystep5'), + url(r'^applystep6/$', login_required(views.applystep6), name='applystep6'), + + + url(r'^view-course$', login_required(views.CourseFeatureView.as_view()), name='view-course'), + url(r'^view-match-result$', login_required(views.MatchResultView.as_view()), name='view-match-result'), + + url(r'^addcoursefunc/$', login_required(views.addcourse), name='addcoursefunc'), + url(r'^addStudentRandomfunc/$', views.addStudentRandom, name='addStudentRandomfunc'), + url(r'^matchfunc/$', login_required(views.StartMatch), name='matchfunc'), + + url(r'^searchcoursefunc/$', login_required(views.searchCourse), name='searchcoursefunc'), + url(r'^changefeaturefunc/$', login_required(views.changeFeature), name='changefeaturefunc'), + + url(r'^view_application$', login_required(views.viewApplictionView.as_view()), name='view_application'), + url(r'^view-students$', login_required(views.viewStudentsView.as_view()), name='view-students'), + url(r'^download-mentor-csv/$', login_required(views.download_mentor_csv), name='download-mentor-csv'), + url(r'^withdrawfunc/$', views.withdraw, name='withdrawfunc'), + #url(r'^apply_prefernece/$', login_required(views.CourseAutocomplete.as_view()), name='apply_prefernece'), + +] diff --git a/compsocsite/mentors/views.py b/compsocsite/mentors/views.py new file mode 100644 index 00000000..db53117e --- /dev/null +++ b/compsocsite/mentors/views.py @@ -0,0 +1,712 @@ +from .models import * +from appauth.models import * +from groups.models import * +import datetime +import os +import time +import collections +import dateutil.parser + +from django.shortcuts import render, get_object_or_404, redirect +from django.http import HttpResponseRedirect, HttpResponse, HttpRequest +from django.urls import reverse +from django import views +from django.db.models import Q + +from django.utils import timezone +from django.template import RequestContext +from django.shortcuts import render_to_response +from django.contrib import messages +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required +from django.core import mail +from django.conf import settings +from django.template import Context + +from django.http import HttpResponseRedirect +from django.shortcuts import render +from .forms import * + +import json +import threading +import itertools +import numpy as np +import random +import csv +import logging +import random as r +from .match import Matcher +import ast + +# Main Page of mentor application +class IndexView(views.generic.ListView): + template_name = 'mentors/index.html' + def get_context_data(self, **kwargs): + ctx = super(IndexView, self).get_context_data(**kwargs) + # check if there exist a mentor application + ctx['applied'] = self.request.user.userprofile.mentor_applied + ctx['admin'] = isMentorAdmin(self.request) + return ctx + + def get_queryset(self): + return Mentor.objects.all() + +def viewindex(request): + return render(request, 'mentors/index.html', {'applied': request.user.userprofile.mentor_applied, 'admin': isMentorAdmin(request)}) + +class ApplyView(views.generic.ListView): + template_name = 'mentors/apply.html' + def get_context_data(self, **kwargs): + ctx = super(ApplyView, self).get_context_data(**kwargs) + ctx['applied'] = self.request.user.userprofile.mentor_applied + + return ctx + def get_queryset(self): + return Mentor.objects.all() + +class viewApplictionView(views.generic.ListView): + template_name = 'mentors/view_application.html' + def get_context_data(self, **kwargs): + ctx = super(viewApplictionView, self).get_context_data(**kwargs) + ctx['user'] = self.request.user.userprofile.mentor_profile + return ctx + def get_queryset(self): + return Mentor.objects.all() + +# Admin to view all the applications +class viewStudentsView(views.generic.ListView): + template_name = 'mentors/view_students.html' + def get_context_data(self, **kwargs): + ctx = super(viewStudentsView, self).get_context_data(**kwargs) + ctx['isAdmin'] = isMentorAdmin(self.request) + ctx['applicants'] = Mentor.objects.all() + ctx['applicants_number'] = len(Mentor.objects.all()) + return ctx + def get_queryset(self): + return Mentor.objects.all() + +# view course features and change their weights +class CourseFeatureView(views.generic.ListView): + template_name = 'mentors/course_feature.html' + def get_context_data(self, **kwargs): + ctx = super(CourseFeatureView, self).get_context_data(**kwargs) + ctx['courses'] = Course.objects.all() + return ctx + def get_queryset(self): + return Course.objects.all() + +class MatchResultView(views.generic.ListView): + template_name = 'mentors/view_match_result.html' + def get_context_data(self, **kwargs): + ctx = super(MatchResultView, self).get_context_data(**kwargs) + ctx['isAdmin'] = isMentorAdmin(self.request) + ctx['courses'] = Course.objects.all() + ctx['result'] = viewMatchResult() + return ctx + def get_queryset(self): + return Course.objects.all() + + +# apply step +def applystep(request): + + this_user = request.user.userprofile + p = this_user.mentor_profile + + initial={ + 'RIN': p.RIN if this_user.mentor_applied else request.session.get('RIN', None), + 'first_name': p.first_name if this_user.mentor_applied else request.session.get('first_name', None), + 'last_name': p.last_name if this_user.mentor_applied else request.session.get('last_name', None), + 'GPA': p.GPA if this_user.mentor_applied else request.session.get('GPA', None), + 'email': request.user.email, + 'phone': p.phone if this_user.mentor_applied else request.session.get('phone', None), + 'recommender': p.recommender if this_user.mentor_applied else request.session.get('recommender', None) + } + + form = MentorApplicationfoForm_step1(request.POST or None, initial=initial) + if request.method == 'POST': + # initate a new mentor applicant + if form.is_valid(): + if (this_user.mentor_applied): + p.RIN = form.cleaned_data['RIN'] + p.first_name = form.cleaned_data['first_name'] + p.last_name = form.cleaned_data['last_name'] + p.GPA = form.cleaned_data['GPA'] + p.email = request.user.email, + p.phone = form.cleaned_data['phone'] + p.recommender = form.cleaned_data['recommender'] + p.save() + else: + request.session['RIN'] = form.cleaned_data['RIN'] + request.session['first_name'] = form.cleaned_data['first_name'] + request.session['last_name'] = form.cleaned_data['last_name'] + request.session['GPA'] = form.cleaned_data['GPA'] + request.session['email'] = request.user.email, + request.session['phone'] = form.cleaned_data['phone'] + request.session['recommender'] = form.cleaned_data['recommender'] + ''' + pref = new_applicant.course_pref + pref[new_applicant.RIN] = order_str + pref.save() + + l = pref[new_applicant.RIN] + l = [n.strip() for n in ast.literal_eval(l)] # convert str list to actual list u['a', 'b', 'c'] -> ['a', 'b', 'c'] + ''' + return HttpResponseRedirect(reverse('mentors:applystep2')) + #return render(request, 'mentors/index.html', {'applied': True}) + + else: + print(form.errors) + + #return HttpResponseRedirect(reverse('groups:members', args=(group.id,))) + + return render(request, 'mentors/apply.html', {'apply_form': form}) + + +# Compensation agreement +def applystep2(request): + if (checkPage(request, 2)): + return checkPage(request, 2) + this_user = request.user.userprofile + p = this_user.mentor_profile + initial={ + 'compensation': p.compensation if this_user.mentor_applied else request.session.get('compensation', None), + 'studnet_status': p.studnet_status if this_user.mentor_applied else request.session.get('studnet_status', None), + 'employed_paid_before': p.employed_paid_before if this_user.mentor_applied else request.session.get('employed_paid_before', None) + } + + form = MentorApplicationfoForm_step2(request.POST or None, initial=initial) + + if request.method == 'POST': + if form.is_valid(): + if (this_user.mentor_applied): + p.compensation = form.cleaned_data['compensation'] + p.studnet_status = form.cleaned_data['studnet_status'] + p.employed_paid_before = form.cleaned_data['employed_paid_before'] + p.save() + else: + request.session['compensation'] = form.cleaned_data['compensation'] + request.session['studnet_status'] = form.cleaned_data['studnet_status'] + request.session['employed_paid_before'] = form.cleaned_data['employed_paid_before'] + + return HttpResponseRedirect(reverse('mentors:applystep3')) + else: + print(form.errors) + + return render(request, 'mentors/apply.html', {'apply_form': form}) + + +# Course grade and mentor experience +def applystep3(request): + if (checkPage(request, 3)): + return checkPage(request, 3) + this_user = request.user.userprofile + p = this_user.mentor_profile + + initial={ 'mentored_non_cs_bf': p.mentored_non_cs_bf if this_user.mentor_applied else request.session.get('mentored_non_cs_bf', None)} + if (this_user.mentor_applied): + for course in Course.objects.all(): + this_grade = Grade.objects.filter(course = course, student = p).first() + course_grade = course.name + "_grade" + course_exp = course.name + "_exp" + initial.update({course_grade: this_grade.student_grade}) + initial.update({course_exp: 'N' if this_grade.mentor_exp == False else 'Y'}) + else: + for course in Course.objects.all(): + course_grade = course.name + "_grade" + course_exp = course.name + "_exp" + initial.update({course_grade: request.session.get(course_grade, 'n')}) + initial.update({course_exp: request.session.get(course_exp, 'N')}) + + + form = MentorApplicationfoForm_step3(request.POST or None, initial=initial) + + if request.method == 'POST': + if form.is_valid(): + if (this_user.mentor_applied): + p.mentored_non_cs_bf = form.cleaned_data['mentored_non_cs_bf'] + for course in Course.objects.all(): + course_grade = course.name + "_grade" + course_exp = course.name + "_exp" + this_grade = Grade.objects.filter(course = course, student = p).first() + this_grade.student_grade = form.cleaned_data[course_grade] # Grade on this course + this_grade.mentor_exp = True if form.cleaned_data[course_exp] == 'Y' else False + this_grade.save() + # if the student has failed the class, or is progressing, we consider he did not take the course + if (this_grade.student_grade != 'p' and this_grade.student_grade != 'n' and this_grade.student_grade != 'f'): + this_grade.have_taken = True + else: + this_grade.have_taken = False + this_grade.save() + else: + request.session['mentored_non_cs_bf'] = form.cleaned_data['mentored_non_cs_bf'] + for course in Course.objects.all(): + course_grade = course.name + "_grade" + course_exp = course.name + "_exp" + request.session[course_grade] = form.cleaned_data[course_grade] + request.session[course_exp] = form.cleaned_data[course_exp] + return HttpResponseRedirect(reverse('mentors:applystep4')) + else: + print(form.errors) + return render(request, 'mentors/apply.html', {'apply_form': form}) + + +# Course preference +def applystep4(request): + if (checkPage(request, 4)): + return checkPage(request, 4) + this_user = request.user.userprofile + p = this_user.mentor_profile + + initial={ 'pref_order': p.course_pref if this_user.mentor_applied else request.session.get('pref_order', None),} + prefer_list = ast.literal_eval(p.course_pref if this_user.mentor_applied else request.session.get('pref_order', '[]')) + + course_list = [c.name for c in Course.objects.all()] + pref_courses = Course.objects.filter(name__in=prefer_list) + not_pref_courses = Course.objects.filter(name__in=[item for item in course_list if item not in prefer_list]) + + #print([i.name for i in pref_courses]) + #print([i.name for i in not_pref_courses]) + + #pref_course = Course.objects.filter(name in set(pref_order)) + form = MentorApplicationfoForm_step4(request.POST or None, initial = initial) + if request.method == 'POST': + if form.is_valid(): + + if (this_user.mentor_applied): + p.course_pref = form.cleaned_data['pref_order'] + p.save() + else: + request.session['pref_order'] = (form.cleaned_data['pref_order']) + #print(request.session['pref_order']) + #print(breakties(form.cleaned_data['pref_order'])) + return HttpResponseRedirect(reverse('mentors:applystep5')) + else: + print(form.errors) + return render(request, 'mentors/apply.html', {'pref_courses': pref_courses, 'not_pref_courses': not_pref_courses, 'courses':Course.objects.all(), 'apply_form': form}) + + +# Time slots page +def applystep5(request): + if (checkPage(request, 5)): + return checkPage(request, 5) + this_user = request.user.userprofile + p = this_user.mentor_profile + initial={ + 'time_slots': p.time_slots if this_user.mentor_applied else request.session.get('time_slots', None), + 'other_times': p.other_times if this_user.mentor_applied else request.session.get('other_times', None), + } + + form = MentorApplicationfoForm_step5(request.POST or None, initial=initial) + if request.method == 'POST': + if form.is_valid(): + #order_str = breakties(request.POST['pref_order']) + if (this_user.mentor_applied): + p.time_slots = form.cleaned_data['time_slots'] + p.other_times = form.cleaned_data['other_times'] + p.save() + else: + request.session['time_slots'] = form.cleaned_data['time_slots'] + request.session['other_times'] = form.cleaned_data['other_times'] + + return HttpResponseRedirect(reverse('mentors:applystep6')) + else: + print(form.errors) + return render(request, 'mentors/apply.html', {'courses':Course.objects.all(), 'apply_form': form}) + + +# Students Additional Page +def applystep6(request): + if (checkPage(request, 6)): + return checkPage(request, 6) + this_user = request.user.userprofile + p = this_user.mentor_profile + initial={ 'relevant_info': p.relevant_info if this_user.mentor_applied else request.session.get('relevant_info', None),} + + form = MentorApplicationfoForm_step6(request.POST or None, initial=initial) + if request.method == 'POST': + if form.is_valid(): + if (this_user.mentor_applied): + p.relevant_info = form.cleaned_data['relevant_info'] + p.save() + else: + request.session['relevant_info'] = form.cleaned_data['relevant_info'] + submit_application(request) # Sumbit a new application + return HttpResponseRedirect(reverse('mentors:index')) + else: + print(form.errors) + return render(request, 'mentors/apply.html', {'apply_form': form}) + + +# return the prefer after brutally break ties +def breakties(order_str): + l = [] + order = getPrefOrder(order_str) # change str to double lists + for i in range(len(order)): + random.shuffle(order[i]) # break ties with random + for j in range(len(order[i])): + l.append(order[i][j]) + + return l + + +# Check whether the student finsihed the previous part of the form to prevent the website crash +def checkPage(request, page): + if (not request.user.userprofile.mentor_applied): + if (page == 2): + keys = ['RIN', 'first_name', 'last_name', 'GPA', 'email', 'phone', 'recommender'] + elif (page == 4 or page == 3): + keys = ['RIN', 'first_name', 'last_name', 'GPA', 'email', 'phone', 'recommender', 'compensation', 'studnet_status', 'employed_paid_before'] + elif (page == 5): + keys = ['RIN', 'first_name', 'last_name', 'GPA', 'email', 'phone', 'recommender','compensation', 'studnet_status', 'employed_paid_before', 'pref_order'] + elif (page == 6): + keys = ['RIN', 'first_name', 'last_name', 'GPA', 'email', 'phone', 'recommender','compensation', 'studnet_status', 'employed_paid_before', 'pref_order', 'time_slots', 'other_times'] + + for k in keys: + if (request.session.get(k, None) == None): + return HttpResponseRedirect(reverse('mentors:index')) + return False + + +def submit_application(request): + new_applicant = Mentor() + + new_applicant.RIN = request.session["RIN"] + new_applicant.first_name = request.session["first_name"] + new_applicant.last_name = request.session["last_name"] + new_applicant.GPA = request.session["GPA"] + new_applicant.phone = request.session["phone"] + new_applicant.recommender = request.session["recommender"] + new_applicant.email = request.session["email"] + + new_applicant.compensation = request.session["compensation"] + new_applicant.studnet_status = request.session["studnet_status"] + new_applicant.employed_paid_before = request.session["employed_paid_before"] + + new_applicant.mentored_non_cs_bf = request.session["mentored_non_cs_bf"] + new_applicant.course_pref = request.session["pref_order"] + new_applicant.time_slots = request.session["time_slots"] + new_applicant.other_times = request.session["other_times"] + new_applicant.relevant_info = request.session["relevant_info"] + new_applicant.save() + + # Save the new application to the profile + this_user = request.user.userprofile + this_user.mentor_applied = True + this_user.mentor_profile = new_applicant + this_user.save() + #orderStr = self.cleaned_data["pref_order"] + + # Save Grades on the course average + for course in Course.objects.all(): + course_grade = course.name + "_grade" + course_exp = course.name + "_exp" + + new_grade = Grade() + new_grade.student = new_applicant + new_grade.course = course + new_grade.student_grade = request.session[course_grade] # Grade on this course + new_grade.save() + + if (request.session[course_exp] == 'Y'): # Mentor Experience + new_grade.mentor_exp = True + else: + new_grade.mentor_exp = False + + # if the student has failed the class, or is progressing, we consider he did not take the course + if (new_grade.student_grade != 'p' and new_grade.student_grade != 'n' and new_grade.student_grade != 'f'): + new_grade.have_taken = True + else: + new_grade.have_taken = False + new_grade.save() + #print(new_grade.course.name + ": " + new_grade.student_grade) + return new_applicant + + +# withdraw application, should add semester later +def withdraw(request): + if request.method == 'GET': + try: + request.user.userprofile.mentor_profile.delete() + request.user.userprofile.mentor_profile = None + request.user.userprofile.mentor_applied = False + request.user.userprofile.save() + except Exception as e: + print(e) + print('Can not delete mentor application') + # Clear sessions + # request.session.flush() + return HttpResponseRedirect(reverse('mentors:index')) + +# load CS_Course.csv to generate courses +def addcourse(request): + #Course.objects.all().delete() + #print(new_course.class_title + new_course.class_number + new_course.class_name + "successfully added") + if request.method == 'POST' and request.FILES['myfile']: + file = request.FILES['myfile'] + decoded_file = file.read().decode('utf-8').splitlines() + reader = csv.reader(decoded_file) + + for row in reader: + c, created = Course.objects.get_or_create( + name = row[0], + subject = row[1], + number = row[2], + ) + c.time_slots = row[4] if row[4].strip()!="" else "[]" + c.mentor_cap = row[3] + c.feature_cumlative_GPA = 1 + c.feature_has_taken = 1 + c.feature_course_GPA = 1 + c.feature_mentor_exp = 1 + c.save() + #print(row[0] + " " + row[1] + " " + row[2] +" "+ row[3] + " successfully added/changed.") + + return HttpResponseRedirect(reverse('mentors:index')) + +# Return the course searched +def searchCourse(request): + courses = Course.objects.all() + if request.method == 'POST': + course_name = request.POST.get('courses', False) + choosen_course = Course.objects.filter(name = course_name).first() + time_slots = Context() + time_slots["times"] = [] + for t in ast.literal_eval(choosen_course.time_slots): + time_slots["times"].append(t) + return render(request, 'mentors/course_feature.html', {'courses': courses, 'choosen_course': choosen_course, 'time_slots': time_slots}) + + +# Change the value of features of a selected course +def changeFeature(request): + courses = Course.objects.all() + if request.method == 'POST': + + course_name = request.POST.get('course', False) + + choosen_course = Course.objects.filter(name = course_name).first() + choosen_course.feature_cumlative_GPA = request.POST.get('f1', False) + choosen_course.feature_course_GPA= request.POST.get('f2', False) + choosen_course.feature_has_taken = request.POST.get('f3', False) + choosen_course.feature_mentor_exp = request.POST.get('f4', False) + choosen_course.save() + return render(request, 'mentors/course_feature.html', {'courses': courses, 'choosen_course': choosen_course}) + + + +# Randomly add students with assigned numbers +def addStudentRandom(request): + classes = [course.name for course in Course.objects.all()] + numClass = len(Course.objects.all()) + + if request.method == 'POST': + Mentor.objects.all().delete() + num_students = request.POST['num_students'] + for i in range(int(num_students)): + new_applicant = Mentor() + new_applicant.RIN = str(100000000 + i) + new_applicant.first_name = "student_" + new_applicant.last_name = str(i) + new_applicant.GPA = round(random.uniform(2.0, 4)*100)/100 # simple round + new_applicant.phone = 518596666 + ''' + pref = Dict() + pref.name = new_applicant.RIN + pref.save() + new_applicant.course_pref = pref + new_applicant.save() + new_applicant.course_pref[new_applicant.RIN] = r.sample(classes, r.randint(1, numClass)) + ''' + new_applicant.course_pref = r.sample(classes, r.randint(1, numClass)) + #print(new_applicant.course_pref) + new_applicant.save() + + for course in Course.objects.all(): + new_grade = Grade(id=None) + #glist = ['a','a-','b+','b','b-','c+','c','c-','d+','d','f','p','n'] + glist = ['a','a-','b+','b','b-','c','c+','n'] + + new_grade.student_grade = random.choice(glist) + if (new_grade.student_grade != 'p' and new_grade.student_grade != 'n' and new_grade.student_grade != 'f'): + new_grade.have_taken = True + new_grade.mentor_exp = random.choice([True, False]) + else: + new_grade.have_taken = False + new_grade.mentor_exp = False + + new_grade.course = course + new_grade.student = new_applicant + new_grade.save() + + #print("Add a new student: " + new_applicant.first_name + new_applicant.last_name + ": GPA: " + str(new_applicant.GPA)) + print("students now: " + str(len(Mentor.objects.all()))) + return HttpResponseRedirect(reverse('mentors:index')) + + +def StartMatch(request): + if request.method == 'POST': + grade_weights = { 'a': 4, 'a-': 3.69, + 'b+': 3.33, 'b': 3, 'b-': 2.67, + 'c+': 2.33, 'c': 2, 'c-': 1.67, + 'd+': 1.33, 'd': 1, 'f': 0, + 'p': 0, 'n': 0, 'ap': 4,} + + # begin matching: + studentFeatures = {} + for s in Mentor.objects.all(): + studentFeatures_per_course = {} + #print(s) + for c in Course.objects.all(): + item = Grade.objects.filter(student = s, course = c).first() + studentFeatures_per_course.update( + {c.name:( + s.GPA/4*100, + grade_weights.get(item.student_grade)/4*100, + int(item.have_taken)*100, + int(item.mentor_exp)*100 + ) + } + ) + #print([i.student_grade for i in s.grade_set.all()]) + studentFeatures.update({s.RIN: studentFeatures_per_course}) + + + numFeatures = 4 # number of features we got + classes = [c.name for c in Course.objects.all()] + classCaps = {c.name: c.mentor_cap for c in Course.objects.all()} + students = [s.RIN for s in Mentor.objects.all()] + studentPrefs = {s.RIN: ast.literal_eval(s.course_pref) for s in Mentor.objects.all()} + classFeatures = {c.name: (c.feature_cumlative_GPA, c.feature_course_GPA, c.feature_has_taken, c.feature_mentor_exp) for c in Course.objects.all()} + matcher = Matcher(studentPrefs, studentFeatures, classCaps, classFeatures) + classMatching = matcher.match() + + assert matcher.isStable() + print("matching is stable\n") + + #print out some classes and students + for (course, student_list) in classMatching.items(): + #print(course + ", cap: " + str(classCaps[course]) + ", features: ", classFeatures[course]) + this_course = Course.objects.filter(name = course).first() + + for s_rin in student_list: + this_student = Mentor.objects.filter(RIN = s_rin).first() + item = Grade.objects.filter(student = this_student, course = this_course).first() + + print(" " + s_rin + " cumlative GPA: " + str(this_student.GPA).upper() + " grade: " + item.student_grade.upper() + ", has mentor exp: " + str(item.mentor_exp) ) + + #assign the course to this student + this_student.mentored_course = this_course + this_student.save() + + unmatchedClasses = set(classes) - classMatching.keys() + unmatchedStudents = set(students) - matcher.studentMatching.keys() + #print(f"{len(unmatchedClasses)} classes with no students") + #print(f"{len(unmatchedStudents)} students not in a class") + + return HttpResponseRedirect(reverse('mentors:view-match-result')) + + #return render(request, 'mentors/view_match_result.html', {'result': viewMatchResult()}) + +def viewResultPage(request): + return HttpResponseRedirect(reverse('mentors:view-match-result')) + +def viewMatchResult(): + # create a context to store the results + result = Context() + result["courses"] = [] # list of courses + for course in Course.objects.all(): + mentor_list = [] + for student in course.mentor_set.all(): + item = Grade.objects.filter(student = student, course = course).first() + new_mentor = {"name": student.first_name+" "+student.last_name, "GPA": student.GPA, "grade": item.student_grade.upper(), "Exp": str(item.mentor_exp)} + mentor_list.append(new_mentor) + + result["courses"].append({"name": str(course), + "number": str(course.number), + "features": (course.feature_cumlative_GPA, course.feature_course_GPA, course.feature_has_taken, course.feature_mentor_exp) , + "mentors": mentor_list}) + return result + + +def isMentorAdmin(request): + Admin_Email_List = ["tomjmwang@gmail.com", "zahavg@rpi.edu", "qianj2@rpi.edu", "cheny42@rpi.edu", "xial@rpi.edu", "hulbes@rpi.edu", "goldsd3@rpi.edu" ] + if (request.user.email.strip() in Admin_Email_List): + return True + return False + +# csv download fucntion +def download_mentor_csv(request): + # Create the HttpResponse object with the appropriate CSV header. + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="S20-UG-mentor-applicants.csv"' + + writer = csv.writer(response) + row = ['RIN', 'Last Name', 'First Name', 'Email', 'Phone', 'GPA', 'Recommender', 'Compensation', 'Paid Before?', 'Status', 'Info', 'Mentored_nonCS'] + for course in Course.objects.all(): + course_prefix = course.subject + '_' + course.number + g = course_prefix+'_Grade' + m = course_prefix+'_Mentored' + row.append(g) + row.append(m) + + for course in Course.objects.all(): + for t in ast.literal_eval(course.time_slots): + ct = course.subject + '_' + course.number + '_' + t + row.append(ct) + row.append("Other Times") + writer.writerow(row) + comp = { + "n": "Either", + "p": "Pay", + "c": 'Credit' + } + status = { + "i": "International", + "d": "Domestic", + } + for m in Mentor.objects.all(): + row = [m.RIN, m.first_name, m.last_name, m.email, m.phone, m.GPA, m.recommender, comp[m.compensation], "paidbyrpi" if m.employed_paid_before else "", status[m.studnet_status], m.relevant_info.replace('"', '\'').replace('/[\n\r]+/', ' '), m.mentored_non_cs_bf] + for course in Course.objects.all(): + grade = Grade.objects.filter(course = course, student = m).first() + course_prefix = course.subject + " " + course.number + row.append(grade.student_grade.upper()) + row.append("Yes" if grade.mentor_exp else "") + for course in Course.objects.all(): + for course_time in ast.literal_eval(course.time_slots): + if(course_time in [i.replace('_', ' ') for i in ast.literal_eval( m.time_slots)]): + if (course.name in ast.literal_eval(m.course_pref)): + row.append("wantstomentor") + else: + row.append("") + else: + row.append("") + row.append(m.other_times.replace('/[\n\r]+/', '').replace('"', '\'')) + writer.writerow(row) + + return response + + +# function to get preference order from a string +# String orderStr +# Question question +# return List> prefOrder +def getPrefOrder(orderStr): + # empty string + if orderStr == "": + return None + if ";;|;;" in orderStr: + current_array = orderStr.split(";;|;;") + final_order = [] + length = 0 + for item in current_array: + if item != "": + curr = item.split(";;") + final_order.append(curr) + length += len(curr) + else: + final_order = json.loads(orderStr) + + + return final_order + + diff --git a/compsocsite/polls/templates/polls/base.html b/compsocsite/polls/templates/polls/base.html index 365c4fda..0b79f304 100755 --- a/compsocsite/polls/templates/polls/base.html +++ b/compsocsite/polls/templates/polls/base.html @@ -20,9 +20,10 @@ - - - + + + + @@ -169,7 +170,9 @@
  • Multi-Polls
  • Classes
  • Groups
  • -
  • Sessions
  • +
  • Sessions
  • +
  • Mentor Application
  • + {% if not request.flavour == "mobile" %}
  • diff --git a/compsocsite/polls/templates/polls/index.html b/compsocsite/polls/templates/polls/index.html index 84aa6d1d..ddffd477 100755 --- a/compsocsite/polls/templates/polls/index.html +++ b/compsocsite/polls/templates/polls/index.html @@ -29,6 +29,16 @@ +
    +

    +

    +
    Spring 2020 Undergraduate Programming Mentor Application is now open!
    + APPLY NOW +

    +

    +
    + +
    diff --git a/compsocsite/static/css/shared.css b/compsocsite/static/css/shared.css index 930503f7..fc2413db 100755 --- a/compsocsite/static/css/shared.css +++ b/compsocsite/static/css/shared.css @@ -122,7 +122,7 @@ button:hover .greencolor { .margin-panel-top { margin-top: 12px; } - + @media only screen and (max-device-width: 480px) { .panel-body { padding-left: 1px; @@ -205,6 +205,10 @@ ul.example li.placeholder:before { /** Define arrowhead **/ } +panel-round-box{ + border-radius: 4rem; + color: rgb(88, 54, 54); +} /* image */ img.item { @@ -371,11 +375,14 @@ th { border-radius: 0.3rem; } + +.list-element.ui-selecting { + background-color: rgb(109, 108, 106) +} .list-element.ui-selected { background-color: rgb(176, 197, 214) } - .choice1 li>div { display: inline-block; position: relative; @@ -384,18 +391,26 @@ th { display: inline-block; } -.choice1 li .div-sortablehandle { +.ul-list li>div { + display: inline-block; + position: relative; + width: 100%; + height: 100%; + display: inline-block; +} + +.choice1 li .li-sortablehandle { display: none; opacity: 0.5; - left: -100%; + top: -42px; } -.choice1 li.ui-selected .div-sortablehandle { +.choice1 li.ui-selected .li-sortablehandle { display: inline-block; cursor: default; - width: 100px; - height: 28px; - left: -107%; + width: 125px; + height: 35px; + top: -42px; border-radius: 0.1875rem; } @@ -414,15 +429,15 @@ th { cursor: default; width: 125px; height: 35px; - background-color: rgb(193, 214, 231); + background-color: rgb(157, 171, 182); border-radius: 0.3rem; } .col-placeHolder { white-space: nowrap; - background-color: rgb(193, 214, 231); height: 40px; - width: 400px; + width: 550px; + background-color: rgb(157, 171, 182); padding: 0; margin: 10px 0px; border-radius: 0.3rem; @@ -439,6 +454,14 @@ th { border: 0px solid darkgray; } - +.noselect { + cursor: default; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/compsocsite/static/css/submission_form.css b/compsocsite/static/css/submission_form.css new file mode 100755 index 00000000..2de878fd --- /dev/null +++ b/compsocsite/static/css/submission_form.css @@ -0,0 +1,183 @@ +.accordion-heading{ + font-size: 66px; + color: red; + +} +.my_label { + color: rgb(63, 63, 63); + font-weight: bold; + display: block; + float: left; + width: 33%; + display: inline-block; +} + +label.radio.inline{ + float: left; + padding: 0px 15px; +} + +.form_title{ + font: 200px; + font-weight: bold; + float: left; +} + +.course_title{ + font: 100px; + font-weight: bold; + float: left; + text-decoration: underline; +} + +.course_block{ + width:100%; + height:200px; +} + +.label_special{ + float:left; + color:red; +} +.c_tier { + float: left; + + color: dimgray; + height: 40px; + width: 40px; + text-align: center; + vertical-align: middle; + line-height: 40px; + + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 151px; + font-weight: 900; + font-weight: bold; + cursor: pointer; + margin: 0px 15px 0px 0px; + border-radius: 0.3rem; +} + +.course_choice { + white-space: nowrap; + background:whitesmoke; + height: 40px; + line-height: 40px; + width: 100%; + padding: 0; + border-top: 2px solid rgb(187, 179, 179); + border-radius: 0.1rem; + flex-wrap: wrap; +} + + +.course_choice2 { + white-space: nowrap; + background:whitesmoke; + height: 40px; + line-height: 40px; + width: 100%; + padding: 0; + border-top: 2px solid rgb(187, 179, 179); + border-radius: 0.1rem; + flex-wrap: wrap; +} + +.course-element { + flex: 0 50%; + display: inline-block; + float: left; + margin: 2.5px 4px; + cursor: default; + width: 275px; + height: 35px; + line-height: 35px; + font-size: 15px; + color:black; + border-radius: 0.2rem; +} + +.course-element-still { + flex: 0 50%; + display: inline-block; + float: left; + margin: 2.5px 4px; + cursor: default; + width: 275px; + height: 35px; + line-height: 35px; + font-size: 15px; + color:black; + border-radius: 0.2rem; +} + +.course-element.ui-selecting { + background-color: rgb(109, 108, 106) +} +.course-element.ui-selected { + background-color: rgb(176, 197, 214) +} + +.ui-slider-handle{ + background-color: grey; +} + +.inputline{ + margin: 2px 8px 4px 8px; + display: block; + +} +.textline { + margin: 2px 8px 12px 8px; + display: block; +} + +.controls{ + float: left; +} + +.control-group{ + display: block; + float: left; +} + +.control-label{ + clear: both; + display: block; + width: 128px; + float: left; + text-align: right; + padding-right: 8px; + white-space: nowrap; +} + +.textinput { +} + +.checkbox{ + width:20%; + height:30px; + display: inline-block; + position: relative; + padding-left: 35px; +} + +.emptyspace { + clear: both; + +} + +.vspace { + clear: both; + margin-top: 6px; + padding-bottom: 6px; +} + +.vspacewithline { + clear: both; + margin-top: 12px; + padding-bottom: 11px; + border-top: 1px solid #d5dfe5; +} diff --git a/compsocsite/static/js/course.js b/compsocsite/static/js/course.js new file mode 100644 index 00000000..057ac8d5 --- /dev/null +++ b/compsocsite/static/js/course.js @@ -0,0 +1,1129 @@ +// Helper JavaScript created for the voting page (detail.html) +var record = '[]'; //for recording two col behaviors +var temp_data; +var one_record = '{"one_column":[]}'; +var swit = ""; //for recording users' action on swritching between voting interfaces +var slider_record = '{"slider":[]}'; +var star_record = '{"star":[]}'; +var submissionURL = ""; +var order1 = ""; +var order2 = ""; +var flavor = ""; +var startTime = 0; +var allowTies = false; +var commentTime = ""; +var method = 1; // 1 is twoCol, 2 is oneCol, 3 is Slider +var methodIndicator = "two_column"; +var init_star = false; +var animOffset = 200; // animation speed of ranking UIs, 200 = 0.2s +var top_c_tier_layer = 0; + +function select(item){ + var d = (Date.now() - startTime).toString(); + temp_data = {"item":$(item).attr("id")}; + temp_data["time"] = [d]; + temp_data["rank"] = [dictYesNo()]; + if($(item).children()[0].checked){ + $(item).css('border-color', 'green'); + $(item).css('border-width', '5px'); + $(item).css('margin-top', '1px'); + $(item).css('margin-bottom', '1px'); + $($(item).children()[1]).removeClass('glyphicon-unchecked'); + $($(item).children()[1]).addClass('glyphicon-check'); + $($(item).children()[1]).css('color', "green"); + } + else{ + $(item).css('border-color', 'grey'); + $(item).css('border-width', '1px'); + $(item).css('margin-top', '5px'); + $(item).css('margin-bottom', '9px'); + $($(item).children()[1]).removeClass('glyphicon-check'); + $($(item).children()[1]).addClass('glyphicon-unchecked'); + $($(item).children()[1]).css('color', "grey"); + } + var temp = JSON.parse(record); + temp.push(temp_data); + record = JSON.stringify(temp); + //console.log(record); +} + +function select2(item){ + var d = (Date.now() - startTime).toString(); + temp_data = {"item":$(item).attr("id")}; + temp_data["time"] = [d]; + temp_data["rank"] = [dictYesNo2()]; + if($(item).children()[0].checked){ + $(item).css('border-color', 'green'); + $(item).css('border-width', '5px'); + $(item).css('margin-top', '1px'); + $(item).css('margin-bottom', '1px'); + $($(item).children()[1]).removeClass('glyphicon-unchecked'); + $($(item).children()[1]).addClass('glyphicon-check'); + $($(item).children()[1]).css('color', "green"); + } + else{ + $(item).css('border-color', 'grey'); + $(item).css('border-width', '1px'); + $(item).css('margin-top', '5px'); + $(item).css('margin-bottom', '9px'); + $($(item).children()[1]).removeClass('glyphicon-check'); + $($(item).children()[1]).addClass('glyphicon-unchecked'); + $($(item).children()[1]).css('color', "grey"); + } + var temp = JSON.parse(record); + temp.push(temp_data); + record = JSON.stringify(temp); + //console.log(record); +} + +//Get order of one or two column +function orderYesNo(num){ + if(num == 5){ list = '#yesNoList'; } + if(num == 6){ list = '#singleList'; } + var arr = $(list).children(); + var order = []; + var yes = []; + var no = []; + $.each(arr, function( index, value ){ + if(!(typeof $(value).children()[0] === "undefined")){ + if($(value).children()[0].checked){ yes.push($(value).attr("type")); } + else{ no.push($(value).attr("type")); } + } + }); + if(yes.length != 0){ order.push(yes); } + if(no.length != 0){ order.push(no); } + return order; +} + +// return the ranking based on vote +function orderCol(num){ + var arr; + if(num == 0){ arr = [$('#left-sortable')]; } + if(num == 1){ arr = [$('#left-sortable'), $('#right-sortable')]; } + else if(num == 2){ arr = [$('#left-sortable')]; } + var order = []; + $.each(arr, function( index, value ){ + value.children().each(function( index ){ + if( $( this ).children().size() > 0 ){ + var inner = []; + $( this ).children().each(function( index ){ + if(!$(this).hasClass("c_tier")) inner.push($( this ).attr('type')); + }); + order.push(inner); + } + }); + }); + + return order; +} + + +function orderSlideStar(str){ + var arr = []; + var values = []; + $('.' + str).each(function(i, obj){ + if(str == 'slide'){ + var score = $( this ).slider("option", "value"); + } + else if(str == 'star'){ + var score = parseFloat($( this ).rateYo("option", "rating")); + } + else{ return false; } + var type = $( this ).attr('type') + var bool = 0; + $.each(values, function( index, value ){ + if(value < score){ + values.splice(index, 0, score); + arr.splice(index, 0, [type]); + bool = 1; + return false; + }else if(value == score){ + arr[index].push(type); + bool = 1; + return false; + } + }); + if(bool == 0){ values.push(score); arr.push([type]); } + }); + return arr; +} + +function dictSlideStar(str){ + var arr = []; + var values = []; + var item_type = ".course-element"; + $('.' + str).each(function(i, obj){ + if(str == 'slide'){ + var score = $( this ).slider("option", "value"); + item_type = ".slider_item"; + } + else if(str == 'star'){ + var score = parseFloat($( this ).rateYo("option", "rating")); + item_type = ".star_item"; + } + else{ return false; } + var type = $( this ).attr('type'); + var bool = 0; + //console.log($(item_type + "[type='" + type + "']").attr('id')); + $.each(values, function( index, value ){ + if(value < score){ + var temp = {}; + temp["name"] = $(item_type + "[type='" + type + "']").attr('id'); + temp["score"] = score; + temp["ranked"] = 0; + values.splice(index, 0, score); + arr.splice(index, 0, [temp]); + bool = 1; + return false; + }else if(value == score){ + var temp = {}; + temp["name"] = $(item_type + "[type='" + type + "']").attr('id'); + temp["score"] = score; + temp["ranked"] = 0; + arr[index].push(temp); + bool = 1; + return false; + } + }); + if(bool == 0){ + var temp = {}; + temp["name"] = $(item_type + "[type='" + type + "']").attr('id'); + temp["score"] = score; + temp["ranked"] = 0; + values.push(score); + arr.push([temp]); + } + }); + var i; + for(i = 0; i < arr.length; i++){ + var j; + for(j = 0; j < arr[i].length; j++){ + arr[i][j]["c_tier"] = i+1; + } + } + return arr; +} + +function dictYesNo(){ + var arr = $('#yesNoList').children(); + var order = []; + var yes = []; + var no = []; + $.each(arr, function( index, value ){ + if(!(typeof $(value).children()[0] === "undefined")){ + temp = {}; + temp["name"] = $(value).attr("id"); + temp["ranked"] = 0; + if($(value).children()[0].checked){temp["c_tier"] = 1; yes.push(temp); } + else{temp["c_tier"] = 2; no.push(temp); } + } + }); + if(yes.length != 0){ order.push(yes); } + if(no.length != 0){ order.push(no); } + return order; +} + +function dictYesNo2(){ + var arr = $('.checkbox'); + var order = []; + var yes = []; + var no = []; + var i = 0; + $.each(arr, function( index, value ){ + if(!(typeof $(value).children()[0] === "undefined")){ + temp = {}; + temp["name"] = $(value).attr("id"); + temp["ranked"] = 0; + if(i == 0) { temp["position"] = "(1,1)";} + else if (i == 1){temp["position"] = "(1,2)";} + else if (i == 2){temp["position"] = "(2,1)";} + else{temp["position"] = "(2,2)";} + if($(value).children()[0].checked){temp["c_tier"] = 1; yes.push(temp); } + else{temp["c_tier"] = 2; no.push(temp); } + i++; + } + }); + if(yes.length != 0){ order.push(yes); } + if(no.length != 0){ order.push(no); } + return order; +} + +// User list +function dictCol(num){ + var arr; + if(num == 0){ arr = [$('#left-sortable')]; } + if(num == 1){ arr = [$('#left-sortable'), $('#right-sortable')]; } + else if(num == 2){ arr = [$('#one-sortable')]; } + var order = []; + var c_tier = 1; + var item_type = ".course-element"; + $.each(arr, function( index, value ){ + value.children().each(function( i1 ){ + if( $( this ).children().size() > 0 && $( this ).attr("class") != "top_c_tier"){ + var inner = []; + $( this ).children().each(function( i2 ){ + var temp = {}; + temp["name"] = $(item_type + "[type='" + $( this ).attr('type') + "']").attr('id'); + temp["utility"] = $(item_type + "[type='" + $( this ).attr('type') + "']").attr('title'); + temp["c_tier"] = c_tier; + temp["ranked"] = index; + inner.push(temp); + }); + order.push(inner); + c_tier++; + } + }); + }); + return order; +} + +function twoColSort( order ){ + var html = ""; + var c_tier = 1; + var emptyLine = "
    "; + html += emptyLine; + $.each(order, function(index, value){ + html += "
      #" + c_tier + "
      "; + $.each(value, function(i, v){ + html += "
    • "; + html += $(".course-element[type='" + v.toString() + "']").html(); + html += "
    • "; + }); + html += "
    "; + c_tier ++; + }); + html += emptyLine; + $('#left-sortable').html(html); + $('#right-sortable').html(""); + changeCSS(); + +} + +function oneColSort( order ){ +// var html = "
      "; + var html = ""; + var c_tier = 1; + var emptyLine = "
      "; + html += emptyLine; + $.each(order, function(index, value){ + html += "
        #" + c_tier + "
        "; + $.each(value, function(i, v){ + html += "
      • "; + html += $("#oneColSection .course-element[type='" + v.toString() + "']").html(); + html += "
      • "; + }); + html += "
      "; + c_tier ++; + + }); + //html += "
        "; + html += emptyLine; + $('#one-sortable').html(html); + changeCSS(); + +} + +function sliderSort( order ){ + $.each(order, function(index, value){ + $.each(value, function(i, v){ + $(".slide[type='" + v.toString() + "']").slider("value", Math.round(100 - (100 * index / order.length))); + $("#score" + $(".slide[type='" + v.toString() + "']").attr("id")).text(Math.round(100 - (100 * index / order.length))); + }); + }); +} + +function sliderZeroSort( order ){ + $.each(order, function(index, value){ + $.each(value, function(i, v){ + $(".slide[type='" + v.toString() + "']").slider("value", 0); + $("#score" + $(".slide[type='" + v.toString() + "']").attr("id")).text(0); + }); + }); +} + +function starSort( order ){ + init_star = true; + $.each(order, function(index, value){ + $.each(value, function(i, v){ + if(index >= 10){ $(".star[type='" + v.toString() + "']").rateYo("option", "rating", 0); } + else{ $(".star[type='" + v.toString() + "']").rateYo("option", "rating", Math.round(10 - (10 * index / Math.min(order.length, 10))) / 2); } + }); + }); + init_star = false; +} + +function yesNoSort( num, order ){ + $.each(order, function(index, value){ + $.each(value, function(i, v){ + var cb; + if(num == 5){ cb = ".checkbox[type='"; } + if(num == 6){ cb = ".checkbox_single[type='"; } + if(index == 0){ + $($(cb + v.toString() + "']").children()[0]).attr('checked', 'checked'); + $($(cb + v.toString() + "']").children()[1]).removeClass('glyphicon-unchecked'); + $($(cb + v.toString() + "']").children()[1]).addClass('glyphicon-check'); + $($(cb + v.toString() + "']").children()[1]).css('color', "green"); + $(cb + v.toString() + "']").css('border-color', 'green'); + $(cb + v.toString() + "']").css('border-width', '5px'); + $(cb + v.toString() + "']").css('margin-top', '1px'); + $(cb + v.toString() + "']").css('margin-bottom', '1px'); + } + else{ + $($(cb + v.toString() + "']").children()[0]).removeAttr('checked'); + $($(cb + v.toString() + "']").children()[1]).removeClass('glyphicon-check'); + $($(cb + v.toString() + "']").children()[1]).addClass('glyphicon-unchecked'); + $($(cb + v.toString() + "']").children()[1]).css('color', "grey"); + $(cb + v.toString() + "']").css('border-color', 'grey'); + $(cb + v.toString() + "']").css('border-width', '1px'); + $(cb + v.toString() + "']").css('margin-top', '5px'); + $(cb + v.toString() + "']").css('margin-bottom', '9px'); + } + }); + }); +} + +function yesNoZeroSort( order ){ + $.each(order, function(index, value){ + $.each(value, function(i, v){ + $($(".checkbox[type='" + v.toString() + "']").children()[0]).removeAttr('checked'); + $($(".checkbox[type='" + v.toString() + "']").children()[1]).removeClass('glyphicon-check'); + $($(".checkbox[type='" + v.toString() + "']").children()[1]).addClass('glyphicon-unchecked'); + $($(".checkbox[type='" + v.toString() + "']").children()[1]).css('color', "grey"); + $(".checkbox[type='" + v.toString() + "']").css('border-color', 'grey'); + $(".checkbox[type='" + v.toString() + "']").css('border-width', '1px'); + $(".checkbox[type='" + v.toString() + "']").css('margin-top', '5px'); + $(".checkbox[type='" + v.toString() + "']").css('margin-bottom', '9px'); + }); + }); +} + + +// change the behavior of the UI when the user change voting method +// or drop a new item +function changeCSS(){ + // if method is twocol + if(method == 1){ + $(".course_choice").css("width", "100%"); + $(".empty"). css("width", "100%"); + $(".col-placeHolder").css("width", "100%"); + + // extend the height of course_choice box if there exists more than 3 course-element in a row + // vice versa + $("#left-sortable").children(".course_choice").each(function(){ + size = $(this).children(":not(.ui-selected, .transporter)").size(); + //$(this).css("height", ((size-1)*40).toString() + "px"); + if(allowTies){ + if(size > 4){ + size -= 1; + num = Math.ceil(size/3); + $(this).css("height", (num*40).toString() + "px"); + $(this).children(".c_tier").css( "height", (num*40).toString() + "px"); + $(this).children(".c_tier").css( "line-height", (num*40).toString() + "px"); + } + else{ + $(this).css("height", "40px"); + $(this).children(".c_tier").css( "height", "40px"); + $(this).children(".c_tier").css( "line-height", "40px"); + + } + } + + }); + } + // if method is onecol, extend the selection bar + else if(method == 2){ + $(".course_choice").css("width", "100%"); + $(".empty"). css("width", "100%"); + $(".col-placeHolder").css("width", "100%"); + } + // extend the height of course_choice box if there exists more than 6 course-element in a row + // vice versa + $("#left-sortable").children(".course_choice").each(function(){ + size = $(this).children(":not(.ui-selected, .transporter)").size(); + if(allowTies){ + + if(size > 5){ + size -= 1; + num = Math.ceil(size/4); + $(this).css("height", (num*40).toString() + "px"); + $(this).children(".c_tier").css( "height", (num*40).toString() + "px"); + $(this).children(".c_tier").css( "line-height", (num*40).toString() + "px"); + } + else{ + $(this).css("height", "40px"); + $(this).children(".c_tier").css( "height", "40px"); + $(this).children(".c_tier").css( "line-height", "40px"); + } + } + }); + +} + +function changeMethod (value){ + var order; + var d = Date.now() - startTime; + if(method == 1){ + swit += d + ";1"; + order = orderCol(method); + + }else if(method == 2){ + swit += d + ";2"; + order = orderCol(method); + } + else if(method == 3){ + swit += d + ";3"; + order = orderSlideStar('slide'); + } + else if(method == 4){ + swit += d + ";4"; + order = orderSlideStar('star'); + } + else if(method == 5 || method == 6){ + order = orderYesNo(method); + } + method = value; + removeSelected(); + changeCSS(); + + if(method == 1){ swit += ";1;;"; methodIndicator = "two_column"; twoColSort(order); } + else if(method == 2){ swit += ";2;;"; methodIndicator = "one_column"; oneColSort(order); } + else if(method == 3){ swit += ";3;;"; methodIndicator = "slider"; sliderSort(order); } + else if(method == 4){ swit += ";4;;"; methodIndicator = "star"; init_star = true; starSort(order); init_star = false;} + else if(method == 5 || method == 6){ yesNoSort(method, order); } +}; + +function recordCommentTime(){ + if(commentTime == ""){ + var d = Date.now() - startTime ; + commentTime += d; + } + +} +// the VoteUtil object contains all the utility functions for the voting UI +var VoteUtil = (function () { + // returns true if the user is on a mobile device, else returns false + function isMobileAgent () { + return /Android|webOS|iPhone|iPod|greyBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + } + + // clears all items from the left side and returns the right side to its default state + function clearAll() { + var d = (Date.now() - startTime).toString(); + temp_data = { + "item": "" + }; + temp_data["time"] = [d]; + temp_data["rank"] = [dictCol(1)]; + if (method == 1) { + var c_tier = $("#right-sortable").children().size()+1; + // move the left items over to the right side + $("#left-sortable").children().each(function(index) { + if ($(this).children().size() > 0) { + $(this).children().each(function(index) { + var temp = $("#right-sortable").html(); + //$(this).attr("onclick")="VoteUtil.moveToPref(this)"; + if (!$(this).hasClass("c_tier")) { + $("#right-sortable").html( + temp + "
          " + $(this)[0].outerHTML + "
        "); + c_tier++; + } + }); + } + }); + // clear the items from the left side + + $('#left-sortable').html(""); + + $(".course_choice").each(function(index) { + $(this).attr("onclick", "VoteUtil.moveToPref(this)"); + }); + + disableSubmission(); + // add the clear action to the record + //var d = Date.now() - startTime; + //record += d + "||"; + d = (Date.now() - startTime).toString(); + temp_data["time"].push(d); + temp_data["rank"].push(dictCol(1)); + var temp = JSON.parse(record); + temp.push(temp_data); + //temp["star"].push({"time":d, "action":"set", "value":rating.toString(), "item":$(this).parent().attr("id") }); + record = JSON.stringify(temp); + + } + } + + // submits the current left side preferences + function submitPref() { + var order; + var order_list; + var final_list; + var item_type = ".course-element"; + var record_data = {}; + $(".top_c_tier").remove(); + if(method == 1) {order_list = orderCol(0); final_list = dictCol(1);} + else if(method == 2){ order_list = orderCol(method); final_list = dictCol(2);} + else if(method == 3){ order_list = orderSlideStar('slide'); item_type = ".slider_item"; final_list = dictSlideStar('slide');} + else if(method == 4){ order_list = orderSlideStar('star'); item_type= ".star_item";final_list = dictSlideStar('star');} + else if(method == 5){ order_list = orderYesNo(method); item_type= ".checkbox"; final_list = dictYesNo();} + else if(method == 6){ order_list = orderYesNo(method); item_type= ".checkbox_single";} + else{location.reload(); } + var final_order = []; + for (var i = 0; i < order_list.length; i++) { + var samec_tier = []; + for (var j = 0; j < order_list[i].length; j++) { + final_order.push( order_list[i][j].toString() ); + } + //final_order.push(samec_tier); + } + order = JSON.stringify(final_order); + //var d = Date.now() - startTime; + //record += "S" + d; + var record_final = JSON.stringify(final_list); + var d = (Date.now() - startTime).toString(); + + record_data["data"] = JSON.parse(record); + record_data["submitted_ranking"] = final_list; + if(order1 != ""){ + record_data["initial_ranking"] = JSON.parse(order1); + } + else{ + record_data["initial_ranking"] = []; + } + record_data["time_submission"] = d; + record_data["platform"] = flavor; + var record_string = JSON.stringify(record_data); + $('.record_data').each(function(){ + $(this).val(record_string); + }); + + $('.pref_order').each(function(){ + $(this).val(order); + }); + + /* + $.ajax({ + url: submissionURL, + type: "POST", + data: {'data': record, 'csrfmiddlewaretoken': $('input[name="csrfmiddlewaretoken"]').val(), 'order1':order1,'final':record_final,'device':flavor,'commentTime':commentTime,'swit':swit,'submit_time':d,'ui':methodIndicator}, + success: function(){} + }); + */ + $('.submitbutton').css( "visibility","hidden"); + $('.submitting').css("visibility","visible"); + console.log(order_list); + + $('#pref_order').submit(); + }; + + // moves preference item obj from the right side to the bottom of the left side + function moveToPref(obj) { + $(obj).removeClass('course_choice2'); + $(obj).addClass('course_choice'); + var time = 100 + var prefcolumn = $('#left-sortable'); + var tier = "
        0
        "; + var currentli = $(obj); + var c_tier = 0; + var c_tierRight = 1; + var c_tierleft = 1; + + var item = currentli.children().first().attr("id"); + var emptyLine = "
        "; + + var d = (Date.now() - startTime).toString(); + + currentli.prepend(tier) + temp_data = { + "item": item + }; + temp_data["time"] = [d]; + temp_data["rank"] = [dictCol(1)]; + + if ($('#left-sortable').children().size() == 0) { + prefcolumn.append(emptyLine); + prefcolumn.append(currentli); + prefcolumn.append(emptyLine); + } else { + $('#left-sortable').children(".course_choice").last().after(currentli); + } + //record += d+ "::clickFrom::" + item + "::"+ c_tier+";;"; + + c_tier = currentli.children().first().attr("alt"); + if ($('#left-sortable').children().size() != 0) { + enableSubmission(); + } + $('#right-sortable').children(".course_choice2").each(function() { + $(this).children(".c_tier").text(c_tierRight.toString()); + c_tierRight++; + }); + $('#left-sortable').children(".course_choice").each(function() { + $(this).children(".c_tier").text(c_tierleft.toString()); + c_tierleft++; + }); + $('#left-sortable').children().each(function() { + $(this).removeAttr('onclick'); + }); + + d = (Date.now() - startTime).toString(); + temp_data["time"].push(d); + temp_data["rank"].push(dictCol(1)); + var temp = JSON.parse(record); + temp.push(temp_data); + record = JSON.stringify(temp); + //d = Date.now() - startTime; + //record += d+ "::clickTo::" + item + "::"+ c_tier+";;;"; + /* + if(methodIndicator == "two_column") + { + var d = (Date.now() - startTime).toString(); + var temp = JSON.parse(record); + temp["two_column"].push({"method":methodIndicator,"time":d, "action":"click", "from":prev_c_tier,"to": c_tier, "item":item }); + record = JSON.stringify(temp); + } + else + { + var d = (Date.now() - startTime).toString(); + var temp = JSON.parse(one_record); + temp["one_column"].push({"method":methodIndicator,"time":d, "action":"click", "from":prev_c_tier,"to": c_tier, "item":item }); + one_record = JSON.stringify(temp); + } + */ + }; + + // moves all items from the right side to the bottom of the left, preserving order + function moveAll() { + var d = (Date.now() - startTime).toString(); + temp_data = {"item":""}; + temp_data["time"] = [d]; + temp_data["rank"] = [dictCol(1)]; + $('.course_choice2').each(function(){ + $(this).removeClass('course_choice2'); + $(this).addClass('course_choice'); + }); + emptyLine = "
        "; + if ($('#right-sortable').children().size() > 0 && $('#left-sortable').children().size() == 0) { + $("#right-sortable").children(".course_choice").each(function(){ + moveToPref($(this)); + }); + } + else if ($('#right-sortable').children().size() > 0 && $('#left-sortable').children().size() > 0){ + $("#right-sortable").children(".course_choice").each(function(){ + moveToPref($(this)); + }); + } + $( '#right-sortable' ).html(""); + //VoteUtil.checkStyle(); + enableSubmission(); + $('.course_choice').each(function(){ + $(this).removeAttr('onclick'); + }); + //var d = Date.now() - startTime; + //record += d + ";;;"; + /* + if(methodIndicator == "two_column") + { + var d = (Date.now() - startTime).toString(); + var temp = JSON.parse(record); + temp["two_column"].push({"method":methodIndicator,"time":d, "action":"moveAll" }); + record = JSON.stringify(temp); + } + else + { + var d = (Date.now() - startTime).toString(); + var temp = JSON.parse(one_record); + temp["one_column"].push({"method":methodIndicator,"time":d, "action":"moveAll" }); + one_record = JSON.stringify(temp); + } + */ + d = (Date.now() - startTime).toString(); + temp_data["time"].push(d); + temp_data["rank"].push(dictCol(1)); + var temp = JSON.parse(record); + temp.push(temp_data); + record = JSON.stringify(temp); + }; + + // enables the submit button + function enableSubmission() { + if( VoteUtil.isMobileAgent() ){ + $(".submitbutton").css("display", "inline"); + }else{ + $(".submitbutton").prop("disabled",false); + } + } + + function disableSubmission(){ + if( VoteUtil.isMobileAgent() ){ + $(".submitbutton").css("display", "none"); + }else{ + $(".submitbutton").prop("disabled",true); + } + } + // returns the public members of the VoteUtil class + return { + isMobileAgent: isMobileAgent, + clearAll: clearAll, + submitPref: submitPref, + moveToPref: moveToPref, + moveAll: moveAll + } + +})() + +// === remove the ui-selected class for each choices === +function removeSelected(){ + $('.course-element').each(function() { + $(this).removeClass("ui-selected"); + }); +} + + + + +$( document ).ready(function() { + !function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery); + + // Google Analytics + // ----------------------------------------------------------------------- + // (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + // (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + // m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + // })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + + // ga('create', 'UA-81006265-1', 'none'); + // //ga('create', 'UA-81006265-1', 'none','DetailTracker'); + // ga('send', 'pageview'); + // ga('send', 'event', 'Button', 'click', 'left-sortable'); + // //ga('DetailTracker.send', 'pageview'); + // ga(function(tracker) { + // // Logs the tracker created above to the console. + // console.log(tracker); + // }); + // var form=document.getElementById('left-sortable'); + // form.addEventListener('submit', function(event) { + + // // Prevents the browser from submiting the form + // // and thus unloading the current page. + // event.preventDefault(); + + // // Sends the event to Google Analytics and + // // resubmits the form once the hit is done. + // ga('send', 'event', 'Left Form', 'submit', { + // hitCallback: function() { + // form.submit(); + // } + // }); + // }); + // // ----------------------------------------------------------------------- + // // Google Tag Manager + // (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': + // new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], + // j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= + // '//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); + // })(window,document,'script','dataLayer','GTM-59SLDM'); + // ----------------------------------------------------------------------- + + /* + var wholeHeight1 = $('#left-sortable')[0].scrollHeight; + var wholeHeight2 = $('#right-sortable')[0].scrollHeight; + if (wholeHeight1 > wholeHeight2) { + $('#right-sortable').css("height", wholeHeight1); + } else { + $('#left-sortable').css("height", wholeHeight2); + } + */ + // $('#left-sortable').sortable('refresh'); + // $('#right-sortable').sortable('refresh'); + + $('.hide1').mouseover(function(){ + $('.ept',this).show(); + }); + //VoteUtil.checkStyle(); + + function enableSubmission() { + $('.submitbutton').css("display", "inline"); + } + if($("#left-sortable").children(".course_choice").size() == 0) {$("#left-sortable").html("");} + // var type_num = 1, alt_num = 0; + // $('#one-sortable').children().each(function(index, value){ + // var string = ""; + // if($(value).children().length > 0){ console.log(alt_num); alt_num += 1; } + // $(value).children().each(function(i, v){ + // string += "
      • "; + // string += $(v).html(); + // string += "
      • \n"; + // type_num += 1; + // }); + // //console.log($(value).html()); + // //console.log(string); + // console.log("string " + string); + // $(value).html(string); + // console.log(value); + // }); + // $('#one-sortable').children().each(function(index, value){ + // //console.log(value); + // }); + + window.setInterval(function(){ + checkSubmission(); + setupSortable(); + }, 1000); + changeCSS(); + + function checkSubmission(){ + if($("#left-sortable").children().size() > 0){ + $(".submitbutton").prop("disabled", false); + } + else{ + $(".submitbutton").prop("disabled", true); + } + } + // reinitalize the sortable function + function setupSortable(){ + var str = ""; + if (allowTies){ + str = ".course_choice, .sortable-ties"; + $('.sortable-ties').selectable({ + cancel: '.c_tier', + filter: "li", + }); + $('.course_choice').sortable({ + cancel: '.c_tier .empty', + cursorAt: { + top: 20, + left: 60 + }, + items: "li:not(.c_tier)", + placeholder: "li-placeHolder", + connectWith: str, + start: function(e, ui){ + //$(".li-placeHolder").hide(animOffset); // add animation for placeholder + }, + helper: function(e, item) { + if (!item.hasClass('ui-selected')) { + $('.ul').find('.ui-selected').removeClass('ui-selected'); + item.addClass('ui-selected'); + } + var selected = $('.ui-selected').clone(); + item.data('multidrag', selected); + $('.ui-selected').not(item).remove(); + + return $('
      • ').append(selected); + }, + receive: function (event, ui) { + changeCSS(); + }, + change: function(e, ui) { + // wait the animation to competer then change teh css of placeholder + $(".li-placeHolder").hide().show(animOffset, function(){ + if (ui.placeholder.parent().hasClass("sortable-ties")) { + if (method == 1) { + $(".li-placeHolder").css("width", "100%"); + } + else if (method == 2) { + $(".li-placeHolder").css("width", "100%"); + } + $(".li-placeHolder").css({ "float": "none", "height": "40px", "margin": "0px 0px" }); + } + else { + $(".li-placeHolder").css({ "float": "left", "width": "125px", "height": "35px", "margin": "2.5px 8px" }); + } + }); + changeCSS(); + }, + + stop: function(e, ui) { + var selected = ui.item.data('multidrag'); + selected.removeClass('ui-selected'); + ui.item.after(selected); + ui.item.remove(); + + checkAll(); + changeCSS(); + removeSelected(); + resetEmpty(); + }, + + + }).disableSelection(); + } + else{ + str = ".sortable-ties"; + } + $('.sortable-ties').sortable({ + placeholder: "col-placeHolder", + //handle: ".c_tier", + //items: "ul", + start: function(e, ui){ + $(".col-placeHolder").hide(); + }, + change: function (e, ui){ + $(".col-placeHolder").hide().show(animOffset, function(){ + if (method == 1) { + $(".col-placeHolder").css("width", "100%"); + } + else if (method == 2) { + $(".col-placeHolder").css("width", "100%"); + } + $(".li-placeHolder").css({ "float": "none", "height": "40px", "margin": "0px 0px" }); + }); + changeCSS(); + }, + stop: function(e, ui) { + checkAll(); + removeSelected(); + resetEmpty(); + changeCSS(); + } + }); + + + } + function checkAll() { + t1 = 1; + t2 = 1; + list = []; + html = "
          "; + if(method == 1){ html += "
          0
          "; } + if(method == 2){ html += "
          0
          "; } + + $('.sortable-ties').children().each(function() { + if ($(this).hasClass('course-element')) { + v = $(this).attr("type"); + //html += "
        • "; + //html += $(this).html(); + + html += "
        • "; + html += $(".course-element[type='" + v + "']").html(); + html += "
        • "; + // $(this).after(newi); + // $(this).remove(); + } + }); + html += "
        "; + $('.sortable-ties').children().each(function() { + if ($(this).hasClass('course-element')) { + $(this).after(html); + return false; //break + } + }); + $('.sortable-ties').children().each(function() { + if ($(this).hasClass('course-element')) { + $(this).remove(); + } + }); + $('.course_choice').each(function() { + if ($(this).children().size() == 1) + $(this).remove(); + }); + + $('.c_tier').each(function() { + $(this).text(t1.toString()); + t1++; + }); + + } + + // Reset the empty lines when a new item is placed + function resetEmpty(){ + $('.sortable-ties').children(".empty").each(function() { + $(this).remove(); + }); + var emptyLine = "
        "; + $('.sortable-ties').prepend(emptyLine); + $('.sortable-ties').append(emptyLine); // since .after() has a bug of not working in certain stances, we use .append() here + + } + //if the user updates existing preferences, the submit button should be shown + if ($('#right-sortable li').length == 0) { + enableSubmission(); + } + + $(".slide").each(function(){ + $(this).slider({ + step: 1, + range: 10, + max: 10, + slide: function( event, ui ) { + + $("#score" + this.id).text(ui.value); + }, + create: function(event, ui){ + var score = document.getElementById('score'+this.id).textContent; + $(this).slider('value', score); + }, + start: function (event, ui){ + var d = (Date.now() - startTime).toString(); + temp_data = {"item":$(this).parent().attr("id")}; + temp_data["time"] = [d]; + temp_data["rank"] = [dictSlideStar("slide")]; + + }, + stop: function (event, ui){ + var d = (Date.now() - startTime).toString(); + temp_data["time"].push(d); + temp_data["rank"].push(dictSlideStar("slide")); + var temp = JSON.parse(record); + temp.push(temp_data); + //temp["slider"].push({"time":d, "action":"stop", "value":ui.value.toString(), "item":$(this).parent().attr("id") }); + record = JSON.stringify(temp); + } + }); + }); + $(".star").each(function(){ + $(this).rateYo({ + numStars: 10, + fullStar: true, + onSet: function (rating, rateYoInstance) { + if(init_star == false) + { + var d = (Date.now() - startTime).toString(); + temp_data = {"item":$(this).parent().attr("id")}; + temp_data["time"] = [d]; + temp_data["rank"] = [dictSlideStar("star")]; + var temp = JSON.parse(record); + temp.push(temp_data); + //temp["star"].push({"time":d, "action":"set", "value":rating.toString(), "item":$(this).parent().attr("id") }); + record = JSON.stringify(temp); + } + } + }); + }); + var t = 1 + $("#twoColSection .course-element").each(function(){ + $(this).attr({type:t.toString()}); + t += 1; + }); + t = 1 + $("#oneColSection .course-element").each(function(){ + $(this).attr({type:t.toString()}); + t += 1; + }); +/* + if(deviceFlavor == "mobile" && firstTime){ + VoteUtil.moveAll(); + } + */ + var limit = 1; + $('.checkbox_single').on('change', function(evt) { + if($(this).children()[0].checked){ + $(this).siblings().each(function(){ + $(this).children()[0].checked = false; + select(this); + }); + }else{ + var ver = false; + $(this).siblings().each(function(){ + select(this); + if($(this).children()[0].checked == true){ + ver = true; + } + }); + if(!ver){ + $(this).children()[0].checked = true; + select(this); + } + } + }); +}); \ No newline at end of file diff --git a/compsocsite/static/js/voting.js b/compsocsite/static/js/voting.js index 37cd6687..ebc18526 100644 --- a/compsocsite/static/js/voting.js +++ b/compsocsite/static/js/voting.js @@ -15,7 +15,7 @@ var commentTime = ""; var method = 1; // 1 is twoCol, 2 is oneCol, 3 is Slider var methodIndicator = "two_column"; var init_star = false; - +var animOffset = 200; // animation speed of ranking UIs, 200 = 0.2s var top_tier_layer = 0; function select(item){ @@ -397,52 +397,59 @@ function yesNoZeroSort( order ){ }); } -function changeCSS(){ - if(method == 1){ - $(".choice1").css("width", "550px"); - $(".empty"). css("width", "550px"); - $(".col-placeHolder").css("width", "550px"); - $("#left-sortable").children(".choice1").each(function(){ - size = $(this).children(":not(.ui-selected, .transporter)").size(); - //$(this).css("height", ((size-1)*40).toString() + "px"); - if(size > 4){ - size -= 1; - num = Math.ceil(size/3); - $(this).css("height", (num*40).toString() + "px"); - $(this).children(".tier").css( "height", (num*40).toString() + "px"); - $(this).children(".tier").css( "line-height", (num*40).toString() + "px"); - } - else{ - $(this).css("height", "40px"); - $(this).children(".tier").css( "height", "40px"); - $(this).children(".tier").css( "line-height", "40px"); +// change the behavior of the UI when the user change voting method +// or drop a new item +function changeCSS(){ + // if method is twocol + if(method == 1){ + $(".choice1").css("width", "550px"); + $(".empty"). css("width", "550px"); + $(".col-placeHolder").css("width", "550px"); - } - }); - } - else if(method == 2){ - $(".choice1").css("width", "800px"); - $(".empty"). css("width", "800px"); - $(".col-placeHolder").css("width", "800px"); - } - $("#one-sortable").children(".choice1").each(function(){ - size = $(this).children(":not(.ui-selected, .transporter)").size(); - if(size > 7){ - size -= 1; - num = Math.ceil(size/6); - $(this).css("height", (num*40).toString() + "px"); - $(this).children(".tier").css( "height", (num*40).toString() + "px"); - $(this).children(".tier").css( "line-height", (num*40).toString() + "px"); + // extend the height of choice1 box if there exists more than 3 list-element in a row + // vice versa + $("#left-sortable").children(".choice1").each(function(){ + size = $(this).children(":not(.ui-selected, .transporter)").size(); + //$(this).css("height", ((size-1)*40).toString() + "px"); + if(size > 4){ + size -= 1; + num = Math.ceil(size/3); + $(this).css("height", (num*40).toString() + "px"); + $(this).children(".tier").css( "height", (num*40).toString() + "px"); + $(this).children(".tier").css( "line-height", (num*40).toString() + "px"); + } + else{ + $(this).css("height", "40px"); + $(this).children(".tier").css( "height", "40px"); + $(this).children(".tier").css( "line-height", "40px"); + } + }); } - else{ - $(this).css("height", "40px"); - $(this).children(".tier").css( "height", "40px"); - $(this).children(".tier").css( "line-height", "40px"); - + // if method is onecol, extend the selection bar + else if(method == 2){ + $(".choice1").css("width", "800px"); + $(".empty"). css("width", "800px"); + $(".col-placeHolder").css("width", "800px"); } - }); + // extend the height of choice1 box if there exists more than 6 list-element in a row + // vice versa + $("#one-sortable").children(".choice1").each(function(){ + size = $(this).children(":not(.ui-selected, .transporter)").size(); + if(size > 7){ + size -= 1; + num = Math.ceil(size/6); + $(this).css("height", (num*40).toString() + "px"); + $(this).children(".tier").css( "height", (num*40).toString() + "px"); + $(this).children(".tier").css( "line-height", (num*40).toString() + "px"); + } + else{ + $(this).css("height", "40px"); + $(this).children(".tier").css( "height", "40px"); + $(this).children(".tier").css( "line-height", "40px"); + } + }); } @@ -854,7 +861,7 @@ $( document ).ready(function() { checkSubmission(); setupSortable(); }, 1000); - changeCSS(); + changeCSS(); function checkSubmission(){ if($("#left-sortable").children().size() > 0){ @@ -863,7 +870,6 @@ $( document ).ready(function() { else{ $(".submitbutton").prop("disabled", true); } - } // reinitalize the sortable function function setupSortable(){ @@ -882,16 +888,28 @@ $( document ).ready(function() { placeholder: "col-placeHolder", handle: ".tier", //items: "ul", - change: function(e, ui) { - if (method == 1) { $(".col-placeHolder").css("width", "550px"); } - else if (method == 2) { $(".col-placeHolder").css("width", "800px"); } + revert:'invalid', + start: function(e, ui){ + $(".col-placeHolder").hide(); + }, + change: function (e,ui){ + $(".col-placeHolder").hide().show(animOffset, function(){ + if (method == 1) { + $(".col-placeHolder").css("width", "550px"); + } + else if (method == 2) { + $(".col-placeHolder").css("width", "800px"); + } + $(".li-placeHolder").css({ "float": "none", "height": "40px", "margin": "0px 0px" }); + }); + changeCSS(); }, stop: function(e, ui) { - checkAll(); - removeSelected(); - resetEmpty(); - changeCSS(); - } + checkAll(); + removeSelected(); + resetEmpty(); + changeCSS(); + } }); $('.choice1').sortable({ @@ -903,7 +921,9 @@ $( document ).ready(function() { items: "li:not(.tier)", placeholder: "li-placeHolder", connectWith: str, - + start: function(e, ui){ + //$(".li-placeHolder").hide(animOffset); // add animation for placeholder + }, helper: function(e, item) { if (!item.hasClass('ui-selected')) { $('.ul').find('.ui-selected').removeClass('ui-selected'); @@ -919,18 +939,21 @@ $( document ).ready(function() { changeCSS(); }, change: function(e, ui) { - if (ui.placeholder.parent().hasClass("sortable-ties")) { - if (method == 1) { - $(".li-placeHolder").css("width", "550px"); - } - else if (method == 2) { - $(".li-placeHolder").css("width", "800px"); + // wait the animation to competer then change teh css of placeholder + $(".li-placeHolder").hide().show(animOffset, function(){ + if (ui.placeholder.parent().hasClass("sortable-ties")) { + if (method == 1) { + $(".li-placeHolder").css("width", "550px"); + } + else if (method == 2) { + $(".li-placeHolder").css("width", "800px"); + } + $(".li-placeHolder").css({ "float": "none", "height": "40px", "margin": "0px 0px" }); } - $(".li-placeHolder").css({ "float": "none", "height": "40px", "margin": "0px 0px" }); - } - else { - $(".li-placeHolder").css({ "float": "left", "width": "125px", "height": "35px", "margin": "2.5px 8px" }); - } + else { + $(".li-placeHolder").css({ "float": "left", "width": "125px", "height": "35px", "margin": "2.5px 8px" }); + } + }); changeCSS(); },