From a74dfdb2e7c49a4e7cbe33eca2dd8451feb7681e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 10 Jan 2013 15:19:38 -0500 Subject: [PATCH 001/125] Remove circular dependencies that connect student.models and django_comment_client --- common/djangoapps/student/models.py | 10 ---------- lms/djangoapps/django_comment_client/models.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 2f5bc3ac04..46e316ac0b 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -262,16 +262,6 @@ class CourseEnrollment(models.Model): return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created) -@receiver(post_save, sender=CourseEnrollment) -def assign_default_role(sender, instance, **kwargs): - if instance.user.is_staff: - role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0] - else: - role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0] - - logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) - instance.user.roles.add(role) - #cache_relation(User.profile) #### Helper methods for use from python manage.py shell. diff --git a/lms/djangoapps/django_comment_client/models.py b/lms/djangoapps/django_comment_client/models.py index 628ac21a4a..a6a2c23603 100644 --- a/lms/djangoapps/django_comment_client/models.py +++ b/lms/djangoapps/django_comment_client/models.py @@ -2,6 +2,10 @@ import logging from django.db import models from django.contrib.auth.models import User +from django.dispatch import receiver +from django.db.models.signals import post_save + +from student.models import CourseEnrollment from courseware.courses import get_course_by_id @@ -45,3 +49,14 @@ class Permission(models.Model): def __unicode__(self): return self.name + + +@receiver(post_save, sender=CourseEnrollment) +def assign_default_role(sender, instance, **kwargs): + if instance.user.is_staff: + role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0] + else: + role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0] + + logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) + instance.user.roles.add(role) From 200493a54f4e7c4ffdd6a7c6d307098ff4aa7782 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 6 Jan 2013 20:57:44 +0000 Subject: [PATCH 002/125] instructor dashboard upgrade - add enrollment management --- ...ed__add_unique_courseenrollmentallowed_.py | 155 +++++++++++++ .../migrations/0021_remove_askbot.py.old | 157 ++++++++++++++ common/djangoapps/student/models.py | 18 ++ lms/djangoapps/instructor/views.py | 205 ++++++++++++++++-- lms/envs/dev.py | 4 + .../courseware/instructor_dashboard.html | 51 ++++- 6 files changed, 567 insertions(+), 23 deletions(-) create mode 100644 common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py create mode 100644 common/djangoapps/student/migrations/0021_remove_askbot.py.old diff --git a/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py b/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py new file mode 100644 index 0000000000..f7e2571685 --- /dev/null +++ b/common/djangoapps/student/migrations/0021_auto__add_courseenrollmentallowed__add_unique_courseenrollmentallowed_.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'CourseEnrollmentAllowed' + db.create_table('student_courseenrollmentallowed', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('email', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), + )) + db.send_create_signal('student', ['CourseEnrollmentAllowed']) + + # Adding unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id'] + db.create_unique('student_courseenrollmentallowed', ['email', 'course_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'CourseEnrollmentAllowed', fields ['email', 'course_id'] + db.delete_unique('student_courseenrollmentallowed', ['email', 'course_id']) + + # Deleting model 'CourseEnrollmentAllowed' + db.delete_table('student_courseenrollmentallowed') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.courseenrollment': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseenrollmentallowed': { + 'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.testcenteruser': { + 'Meta': {'object_name': 'TestCenterUser'}, + 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), + 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] \ No newline at end of file diff --git a/common/djangoapps/student/migrations/0021_remove_askbot.py.old b/common/djangoapps/student/migrations/0021_remove_askbot.py.old new file mode 100644 index 0000000000..89f7208f40 --- /dev/null +++ b/common/djangoapps/student/migrations/0021_remove_askbot.py.old @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +ASKBOT_AUTH_USER_COLUMNS = ( + 'website', + 'about', + 'gold', + 'email_isvalid', + 'real_name', + 'location', + 'reputation', + 'gravatar', + 'bronze', + 'last_seen', + 'silver', + 'questions_per_page', + 'new_response_count', + 'seen_response_count', +) + + +class Migration(SchemaMigration): + + def forwards(self, orm): + "Kill the askbot" + # For MySQL, we're batching the alters together for performance reasons + if db.backend_name == 'mysql': + drops = ["drop `{0}`".format(col) for col in ASKBOT_AUTH_USER_COLUMNS] + statement = "alter table `auth_user` {0};".format(", ".join(drops)) + db.execute(statement) + else: + for column in ASKBOT_AUTH_USER_COLUMNS: + db.delete_column('auth_user', column) + + def backwards(self, orm): + raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.") + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.courseenrollment': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.testcenteruser': { + 'Meta': {'object_name': 'TestCenterUser'}, + 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), + 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), + 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), + 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), + 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), + 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), + 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 2f5bc3ac04..d3254532bc 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -262,6 +262,24 @@ class CourseEnrollment(models.Model): return "[CourseEnrollment] %s: %s (%s)" % (self.user, self.course_id, self.created) +class CourseEnrollmentAllowed(models.Model): + """ + Table of users (specified by email address strings) who are allowed to enroll in a specified course. + The user may or may not (yet) exist. Enrollment by users listed in this table is allowed + even if the enrollment time window is past. + """ + email = models.CharField(max_length=255, db_index=True) + course_id = models.CharField(max_length=255, db_index=True) + + created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) + + class Meta: + unique_together = (('email', 'course_id'), ) + + def __unicode__(self): + return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) + + @receiver(post_save, sender=CourseEnrollment) def assign_default_role(sender, instance, **kwargs): if instance.user.is_staff: diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 2bad058ad8..b74fd495a1 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -2,8 +2,10 @@ from collections import defaultdict import csv +import json import logging import os +import requests import urllib from django.conf import settings @@ -20,7 +22,7 @@ from courseware.courses import get_course_with_access from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA from django_comment_client.utils import has_forum_access from psychometrics import psychoanalyze -from student.models import CourseEnrollment +from student.models import CourseEnrollment, CourseEnrollmentAllowed from xmodule.course_module import CourseDescriptor from xmodule.modulestore import Location from xmodule.modulestore.django import modulestore @@ -125,7 +127,7 @@ def instructor_dashboard(request, course_id): except Exception as err: msg += '

Error: {0}

'.format(escape(err)) - if action == 'Dump list of enrolled students': + if action == 'Dump list of enrolled students' or action=='List enrolled students': log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False) datatable['title'] = 'List of students enrolled in {0}'.format(course_id) @@ -257,6 +259,70 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, '{0} {1} as {2} for {3}'.format(FORUM_ROLE_ADD, uname, FORUM_ROLE_COMMUNITY_TA, course_id), {}, page='idashboard') + #---------------------------------------- + # enrollment + + elif action == 'List students who may enroll but may not have yet signed up': + ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id) + datatable = {'header': ['StudentEmail']} + datatable['data'] = [[x.email] for x in ceaset] + datatable['title'] = action + + elif action == 'Enroll student': + + student = request.POST.get('enstudent','') + datatable = {} + try: + nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id) + nce.save() + msg += "Enrolled student with email '%s'" % student + except Exception as err: + msg += "Error! Failed to enroll student with email '%s'\n" % student + msg += str(err) + '\n' + + elif action == 'Un-enroll student': + + student = request.POST.get('enstudent','') + datatable = {} + try: + nce = CourseEnrollment.objects.get(user=User.objects.get(email=student), course_id=course_id) + nce.delete() + msg += "Un-enrolled student with email '%s'" % student + except Exception as err: + msg += "Error! Failed to un-enroll student with email '%s'\n" % student + msg += str(err) + '\n' + + elif action == 'Un-enroll ALL students': + + ret = _do_enroll_students(course, course_id, '', overload=True) + datatable = ret['datatable'] + + elif action == 'Enroll multiple students': + + students = request.POST.get('enroll_multiple','') + ret = _do_enroll_students(course, course_id, students) + datatable = ret['datatable'] + + elif action == 'List sections available in remote gradebook': + + msg2, datatable = _do_remote_gradebook(course, 'get-sections') + msg += msg2 + + elif action in ['List students in section in remote gradebook', + 'Overload enrollment list using remote gradebook', + 'Merge enrollment list with remote gradebook']: + + section = request.POST.get('gradebook_section','') + msg2, datatable = _do_remote_gradebook(course, 'get-membership', dict(section=section) ) + msg += msg2 + + if not 'List' in action: + students = ','.join([x['email'] for x in datatable['retdata']]) + overload = 'Overload' in action + ret = _do_enroll_students(course, course_id, students, overload=overload) + datatable = ret['datatable'] + + #---------------------------------------- # psychometrics @@ -270,9 +336,9 @@ def instructor_dashboard(request, course_id): problems = psychoanalyze.problems_with_psychometric_data(course_id) - #---------------------------------------- # context for rendering + context = {'course': course, 'staff_access': True, 'admin_access': request.user.is_staff, @@ -285,16 +351,65 @@ def instructor_dashboard(request, course_id): 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), 'djangopid' : os.getpid(), + 'mitx_version' : getattr(settings,'MITX_VERSION_STRING','') } return render_to_response('courseware/instructor_dashboard.html', context) + +def _do_remote_gradebook(course, action, args=None): + ''' + Perform remote gradebook action. Returns msg, datatable. + ''' + rg = course.metadata.get('remote_gradebook','') + if not rg: + msg = "No remote gradebook defined in course metadata" + return msg, {} + + rgurl = settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') + if not rgurl: + msg = "No remote gradebook url defined in settings.MITX_FEATURES" + return msg, {} + + rgname = rg.get('name','') + if not rgname: + msg = "No gradebook name defined in course remote_gradebook metadata" + return msg, {} + + if args is None: + args = {} + data = dict(submit=action, gradebook=rgname) + data.update(args) + + try: + resp = requests.post(rgurl, data=data, verify=False) + retdict = json.loads(resp.content) + except Exception as err: + msg = "Failed to communicate with gradebook server at %s
" % rgurl + msg += "Error: %s" % err + msg += "
resp=%s" % resp.content + msg += "
data=%s" % data + return msg, {} + + msg = '
%s
' % retdict['msg'].replace('\n','
') + retdata = retdict['data'] + + if retdata: + datatable = {'header': retdata[0].keys()} + datatable['data'] = [x.values() for x in retdata] + datatable['title'] = 'Remote gradebook response for %s' % action + datatable['retdata'] = retdata + else: + datatable = {} + + return msg, datatable + def _list_course_forum_members(course_id, rolename, datatable): ''' Fills in datatable with forum membership information, for a given role, so that it will be displayed on instructor dashboard. - course_ID = course's ID string + course_ID = the ID string for a course rolename = one of "Administrator", "Moderator", "Community TA" Returns message status string to append to displayed message, if role is unknown. @@ -455,6 +570,68 @@ def grade_summary(request, course_id): return render_to_response('courseware/grade_summary.html', context) +def _do_enroll_students(course, course_id, students, overload=False): + """Do the actual work of enrolling multiple students, presented as a string + of emails separated by commas or returns""" + + ns = [x.split('\n') for x in students.split(',')] + new_students = [item for sublist in ns for item in sublist] + new_students = [str(s.strip()) for s in new_students] + new_students_lc = [x.lower() for x in new_students] + + if '' in new_students: + new_students.remove('') + + status = dict([x,'unprocessed'] for x in new_students) + + if overload: # delete all but staff + todelete = CourseEnrollment.objects.filter(course_id=course_id) + for ce in todelete: + if not has_access(ce.user, course, 'staff') and ce.user.email.lower() not in new_students_lc: + status[ce.user.email] = 'deleted' + ce.delete() + else: + status[ce.user.email] = 'is staff' + ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_id) + for cea in ceaset: + status[cea.email] = 'removed from pending enrollment list' + ceaset.delete() + + for student in new_students: + try: + user=User.objects.get(email=student) + except User.DoesNotExist: + # user not signed up yet, put in pending enrollment allowed table + if CourseEnrollmentAllowed.objects.filter(email=student, course_id=course_id): + status[student] = 'user does not exist, enrollment already allowed, pending' + continue + cea = CourseEnrollmentAllowed(email=student, course_id=course_id) + cea.save() + status[student] = 'user does not exist, enrollment allowed, pending' + continue + + if CourseEnrollment.objects.filter(user=user, course_id=course_id): + status[student] = 'already enrolled' + continue + try: + nce = CourseEnrollment(user=user, course_id=course_id) + nce.save() + status[student] = 'added' + except: + status[student] = 'rejected' + + datatable = {'header': ['StudentEmail', 'action']} + datatable['data'] = [[x, status[x]] for x in status] + datatable['title'] = 'Enrollment of students' + + def sf(stat): return [x for x in status if status[x]==stat] + + data = dict(added=sf('added'), rejected=sf('rejected')+sf('exists'), + deleted=sf('deleted'), datatable=datatable) + + return data + + @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) def enroll_students(request, course_id): @@ -473,22 +650,10 @@ def enroll_students(request, course_id): course = get_course_with_access(request.user, course_id, 'staff') existing_students = [ce.user.email for ce in CourseEnrollment.objects.filter(course_id=course_id)] - if 'new_students' in request.POST: - new_students = request.POST['new_students'].split('\n') - else: - new_students = [] - new_students = [s.strip() for s in new_students] - - added_students = [] - rejected_students = [] - - for student in new_students: - try: - nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id) - nce.save() - added_students.append(student) - except: - rejected_students.append(student) + new_students = request.POST.get('new_students') + ret = _do_enroll_students(course, course_id, new_students) + added_students = ret['added'] + rejected_students = ret['rejected'] return render_to_response("enroll_students.html", {'course': course_id, 'existing_students': existing_students, diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 058c67fa4d..f5999bf52e 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -102,6 +102,10 @@ SUBDOMAIN_BRANDING = { COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" +################################# mitx revision string ##################### + +MITX_VERSION_STRING = os.popen('cd %s; git describe' % REPO_ROOT).read().strip() + ################################# Staff grading config ##################### STAFF_GRADING_INTERFACE = { diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 74bc25fcbe..bb0dcef970 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -57,10 +57,13 @@ function goto( mode) Psychometrics | %endif Admin | - Forum Admin ] + Forum Admin | + Enrollment + ] -
${djangopid}
+
${djangopid} + | ${mitx_version}
@@ -163,10 +166,52 @@ function goto( mode) %endif %endif +##----------------------------------------------------------------------------- +%if modeflag.get('Enrollment'): + +
+

+ + +

+ Student Email: + + +


+ + %if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: + + <% + rg = course.metadata.get('remote_gradebook',{}) + %> + +

Pull enrollment from remote gradebook

+
    +
  • Gradebook name:
  • +
  • Section:
  • +
+ + + + +
+ + %endif + +

Add students: enter emails, separated by returns or commas;

+ + + +%endif + +##----------------------------------------------------------------------------- +
##----------------------------------------------------------------------------- -%if modeflag.get('Psychometrics') is None: +##----------------------------------------------------------------------------- + +%if datatable and modeflag.get('Psychometrics') is None:

From 97fb05444922c6def563e5fb56c0f3d4599ea0a4 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 00:50:59 +0000 Subject: [PATCH 003/125] add export grades to remote gradebook to instructor dashboard --- lms/djangoapps/instructor/views.py | 96 ++++++++++++++++--- .../courseware/instructor_dashboard.html | 38 +++++++- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b74fd495a1..b5caeac964 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -8,6 +8,8 @@ import os import requests import urllib +from StringIO import StringIO + from django.conf import settings from django.contrib.auth.models import User, Group from django.http import HttpResponse @@ -77,9 +79,12 @@ def instructor_dashboard(request, course_id): data.append(['metadata', escape(str(course.metadata))]) datatable['data'] = data - def return_csv(fn, datatable): - response = HttpResponse(mimetype='text/csv') - response['Content-Disposition'] = 'attachment; filename={0}'.format(fn) + def return_csv(fn, datatable, fp=None): + if fp is None: + response = HttpResponse(mimetype='text/csv') + response['Content-Disposition'] = 'attachment; filename={0}'.format(fn) + else: + response = fp writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer.writerow(datatable['header']) for datarow in datatable['data']: @@ -160,6 +165,65 @@ def instructor_dashboard(request, course_id): track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') return return_csv('answer_dist_{0}.csv'.format(course_id), get_answers_distribution(request, course_id)) + #---------------------------------------- + # export grades to remote gradebook + + elif action=='List assignments available in remote gradebook': + msg2, datatable = _do_remote_gradebook(request.user, course, 'get-assignments') + msg += msg2 + + elif action=='List assignments available for this course': + log.debug(action) + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + + assignments = [[x] for x in allgrades['assignments']] + datatable = {'header': ['Assignment Name']} + datatable['data'] = assignments + datatable['title'] = action + + msg += 'assignments=
%s
' % assignments + + elif action=='List enrolled students matching remote gradebook': + stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False) + msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') + datatable = {'header': ['Student email', 'Match?']} + rg_students = [ x['email'] for x in rg_stud_data['retdata'] ] + def domatch(x): + return 'yes' if x.email in rg_students else 'No' + datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']] + datatable['title'] = action + + elif action in ['Display grades for assignment', 'Export grades for assignment to remote gradebook', + 'Export CSV file of grades for assignment']: + + log.debug(action) + datatable = {} + aname = request.POST.get('assignment_name','') + if not aname: + msg += "Please enter an assignment name" + else: + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + if aname not in allgrades['assignments']: + msg += "Invalid assignment name '%s'" % aname + else: + aidx = allgrades['assignments'].index(aname) + datatable = {'header': ['External email', aname]} + datatable['data'] = [[x.email, x.grades[aidx]] for x in allgrades['students']] + datatable['title'] = 'Grades for assignment "%s"' % aname + + if 'Export CSV' in action: + # generate and return CSV file + return return_csv('grades %s.csv' % aname, datatable) + + elif 'remote gradebook' in action: + fp = StringIO() + return_csv('', datatable, fp=fp) + fp.seek(0) + files = {'datafile': fp} + msg2, dataset = _do_remote_gradebook(request.user, course, 'post-grades', files=files) + msg += msg2 + + #---------------------------------------- # Admin @@ -305,7 +369,7 @@ def instructor_dashboard(request, course_id): elif action == 'List sections available in remote gradebook': - msg2, datatable = _do_remote_gradebook(course, 'get-sections') + msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections') msg += msg2 elif action in ['List students in section in remote gradebook', @@ -313,7 +377,7 @@ def instructor_dashboard(request, course_id): 'Merge enrollment list with remote gradebook']: section = request.POST.get('gradebook_section','') - msg2, datatable = _do_remote_gradebook(course, 'get-membership', dict(section=section) ) + msg2, datatable = _do_remote_gradebook(request.user, course, 'get-membership', dict(section=section) ) msg += msg2 if not 'List' in action: @@ -357,7 +421,7 @@ def instructor_dashboard(request, course_id): return render_to_response('courseware/instructor_dashboard.html', context) -def _do_remote_gradebook(course, action, args=None): +def _do_remote_gradebook(user, course, action, args=None, files=None): ''' Perform remote gradebook action. Returns msg, datatable. ''' @@ -378,11 +442,11 @@ def _do_remote_gradebook(course, action, args=None): if args is None: args = {} - data = dict(submit=action, gradebook=rgname) + data = dict(submit=action, gradebook=rgname, user=user.email) data.update(args) try: - resp = requests.post(rgurl, data=data, verify=False) + resp = requests.post(rgurl, data=data, verify=False, files=files) retdict = json.loads(resp.content) except Exception as err: msg = "Failed to communicate with gradebook server at %s
" % rgurl @@ -392,7 +456,7 @@ def _do_remote_gradebook(course, action, args=None): return msg, {} msg = '
%s
' % retdict['msg'].replace('\n','
') - retdata = retdict['data'] + retdata = retdict['data'] # a list of dicts if retdata: datatable = {'header': retdata[0].keys()} @@ -495,16 +559,18 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username') header = ['ID', 'Username', 'Full Name', 'edX email', 'External email'] + assignments = [] if get_grades and enrolled_students.count() > 0: # just to construct the header gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores) # log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset)) if get_raw_scores: - header += [score.section for score in gradeset['raw_scores']] + assignments += [score.section for score in gradeset['raw_scores']] else: - header += [x['label'] for x in gradeset['section_breakdown']] + assignments += [x['label'] for x in gradeset['section_breakdown']] + header += assignments - datatable = {'header': header} + datatable = {'header': header, 'assignments': assignments, 'students': enrolled_students} data = [] for student in enrolled_students: @@ -518,9 +584,11 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) # log.debug('student={0}, gradeset={1}'.format(student,gradeset)) if get_raw_scores: - datarow += [score.earned for score in gradeset['raw_scores']] + student_grades = [score.earned for score in gradeset['raw_scores']] else: - datarow += [x['percent'] for x in gradeset['section_breakdown']] + student_grades = [x['percent'] for x in gradeset['section_breakdown']] + datarow += student_grades + student.grades = student_grades # store in student object data.append(datarow) datatable['data'] = data diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index bb0dcef970..b2ec220484 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -96,6 +96,42 @@ function goto( mode)

+
+ + %if settings.MITX_FEATURES.get('REMOTE_GRADEBOOK_URL','') and instructor_access: + + <% + rg = course.metadata.get('remote_gradebook',{}) + %> + +

Export grades to remote gradebook

+

The assignments defined for this course should match the ones + stored in the gradebook, for this to work properly!

+ +
    +
  • Gradebook name: ${rg.get('name','None defined!')} +
    +
    + + +
    +
    +
  • +
  • +
    +
    +
  • +
  • Assignment name: +
    +
    + + + +
  • +
+ + %endif + %endif ##----------------------------------------------------------------------------- @@ -187,7 +223,7 @@ function goto( mode)

Pull enrollment from remote gradebook

    -
  • Gradebook name:
  • +
  • Gradebook name: ${rg.get('name','None defined!')}
  • Section:
From 82e31d533b1640836b683e9f408b8d9afb5cc6e7 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 02:03:27 +0000 Subject: [PATCH 004/125] Hookup CourseEnrollmentAllowed to lms/djangoapps/courseware/access.py --- lms/djangoapps/courseware/access.py | 6 ++++++ lms/djangoapps/instructor/views.py | 23 ++++++++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index ba9b8a3bc0..0d4a37eda5 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -5,6 +5,8 @@ like DISABLE_START_DATES""" import logging import time +import student.models + from django.conf import settings from xmodule.course_module import CourseDescriptor @@ -124,6 +126,10 @@ def _has_access_course_desc(user, course, action): debug("Allow: in enrollment period") return True + # if user is in CourseEnrollmentAllowed with right course_id then can also enroll + if user is not None and student.models.CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): + return True + # otherwise, need staff access return _has_staff_access_to_descriptor(user, course) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b5caeac964..0ea3cf0435 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -335,26 +335,27 @@ def instructor_dashboard(request, course_id): elif action == 'Enroll student': student = request.POST.get('enstudent','') - datatable = {} - try: - nce = CourseEnrollment(user=User.objects.get(email=student), course_id=course_id) - nce.save() - msg += "Enrolled student with email '%s'" % student - except Exception as err: - msg += "Error! Failed to enroll student with email '%s'\n" % student - msg += str(err) + '\n' + ret = _do_enroll_students(course, course_id, student) + datatable = ret['datatable'] elif action == 'Un-enroll student': student = request.POST.get('enstudent','') datatable = {} + isok = False + cea = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=student) + if cea: + cea.delete() + msg += "Un-enrolled student with email '%s'" % student + isok = True try: nce = CourseEnrollment.objects.get(user=User.objects.get(email=student), course_id=course_id) nce.delete() msg += "Un-enrolled student with email '%s'" % student except Exception as err: - msg += "Error! Failed to un-enroll student with email '%s'\n" % student - msg += str(err) + '\n' + if not isok: + msg += "Error! Failed to un-enroll student with email '%s'\n" % student + msg += str(err) + '\n' elif action == 'Un-enroll ALL students': @@ -582,7 +583,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, if get_grades: gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) - # log.debug('student={0}, gradeset={1}'.format(student,gradeset)) + log.debug('student={0}, gradeset={1}'.format(student,gradeset)) if get_raw_scores: student_grades = [score.earned for score in gradeset['raw_scores']] else: From 04d6f08c0cd4c901649c90edc0bca71424e2af7b Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 04:17:02 +0000 Subject: [PATCH 005/125] add offline grade computation & DB table for this --- common/djangoapps/student/admin.py | 2 + lms/djangoapps/courseware/access.py | 14 ++- lms/djangoapps/courseware/admin.py | 5 + ..._add_unique_offlinecomputedgrade_user_c.py | 117 ++++++++++++++++++ lms/djangoapps/courseware/models.py | 37 ++++++ .../management/commands/compute_grades.py | 50 ++++++++ .../instructor/offline_gradecalc.py | 103 +++++++++++++++ lms/djangoapps/instructor/views.py | 56 ++++++--- .../courseware/instructor_dashboard.html | 6 + 9 files changed, 367 insertions(+), 23 deletions(-) create mode 100644 lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py create mode 100644 lms/djangoapps/instructor/management/commands/compute_grades.py create mode 100644 lms/djangoapps/instructor/offline_gradecalc.py diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index ec3b708ca7..64fe844801 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -12,6 +12,8 @@ admin.site.register(UserTestGroup) admin.site.register(CourseEnrollment) +admin.site.register(CourseEnrollmentAllowed) + admin.site.register(Registration) admin.site.register(PendingNameChange) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 0d4a37eda5..b58f8d5470 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -5,8 +5,6 @@ like DISABLE_START_DATES""" import logging import time -import student.models - from django.conf import settings from xmodule.course_module import CourseDescriptor @@ -15,6 +13,13 @@ from xmodule.modulestore import Location from xmodule.timeparse import parse_time from xmodule.x_module import XModule, XModuleDescriptor +# student.models imports Role, which imports courseware.access ; use a try, to break the circular import +try: + from student.models import CourseEnrollmentAllowed +except Exception as err: + CourseEnrollmentAllowed = None + + DEBUG_ACCESS = False log = logging.getLogger(__name__) @@ -127,8 +132,9 @@ def _has_access_course_desc(user, course, action): return True # if user is in CourseEnrollmentAllowed with right course_id then can also enroll - if user is not None and student.models.CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): - return True + if user is not None and CourseEnrollmentAllowed: + if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id): + return True # otherwise, need staff access return _has_staff_access_to_descriptor(user, course) diff --git a/lms/djangoapps/courseware/admin.py b/lms/djangoapps/courseware/admin.py index cda4fbb788..f7e54d1800 100644 --- a/lms/djangoapps/courseware/admin.py +++ b/lms/djangoapps/courseware/admin.py @@ -7,3 +7,8 @@ from django.contrib import admin from django.contrib.auth.models import User admin.site.register(StudentModule) + +admin.site.register(OfflineComputedGrade) + +admin.site.register(OfflineComputedGradeLog) + diff --git a/lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py b/lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py new file mode 100644 index 0000000000..674f97cec8 --- /dev/null +++ b/lms/djangoapps/courseware/migrations/0005_auto__add_offlinecomputedgrade__add_unique_offlinecomputedgrade_user_c.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'OfflineComputedGrade' + db.create_table('courseware_offlinecomputedgrade', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), + ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)), + ('gradeset', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('courseware', ['OfflineComputedGrade']) + + # Adding unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id'] + db.create_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id']) + + # Adding model 'OfflineComputedGradeLog' + db.create_table('courseware_offlinecomputedgradelog', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), + ('seconds', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('nstudents', self.gf('django.db.models.fields.IntegerField')(default=0)), + )) + db.send_create_signal('courseware', ['OfflineComputedGradeLog']) + + + def backwards(self, orm): + # Removing unique constraint on 'OfflineComputedGrade', fields ['user', 'course_id'] + db.delete_unique('courseware_offlinecomputedgrade', ['user_id', 'course_id']) + + # Deleting model 'OfflineComputedGrade' + db.delete_table('courseware_offlinecomputedgrade') + + # Deleting model 'OfflineComputedGradeLog' + db.delete_table('courseware_offlinecomputedgradelog') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'courseware.offlinecomputedgrade': { + 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'OfflineComputedGrade'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'gradeset': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'courseware.offlinecomputedgradelog': { + 'Meta': {'object_name': 'OfflineComputedGradeLog'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'nstudents': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'seconds': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'courseware.studentmodule': { + 'Meta': {'unique_together': "(('student', 'module_state_key', 'course_id'),)", 'object_name': 'StudentModule'}, + 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}), + 'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'module_state_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_column': "'module_id'", 'db_index': 'True'}), + 'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}), + 'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['courseware'] \ No newline at end of file diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index ffc7c929de..21ef8b3d66 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -177,3 +177,40 @@ class StudentModuleCache(object): def append(self, student_module): self.cache.append(student_module) + + +class OfflineComputedGrade(models.Model): + """ + Table of grades computed offline for a given user and course. + """ + user = models.ForeignKey(User, db_index=True) + course_id = models.CharField(max_length=255, db_index=True) + + created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) + updated = models.DateTimeField(auto_now=True, db_index=True) + + gradeset = models.TextField(null=True, blank=True) # grades, stored as JSON + + class Meta: + unique_together = (('user', 'course_id'), ) + + def __unicode__(self): + return "[OfflineComputedGrade] %s: %s (%s) = %s" % (self.user, self.course_id, self.created, self.gradeset) + + +class OfflineComputedGradeLog(models.Model): + """ + Log of when offline grades are computed. + Use this to be able to show instructor when the last computed grades were done. + """ + class Meta: + ordering = ["-created"] + get_latest_by = "created" + + course_id = models.CharField(max_length=255, db_index=True) + created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) + seconds = models.IntegerField(default=0) # seconds elapsed for computation + nstudents = models.IntegerField(default=0) + + def __unicode__(self): + return "[OCGLog] %s: %s" % (self.course_id, self.created) diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py new file mode 100644 index 0000000000..717bfd5802 --- /dev/null +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# +# django management command: dump grades to csv files +# for use by batch processes + +import os, sys, string +import datetime +import json + +#import student.models +from instructor.offline_gradecalc import * +from courseware.courses import get_course_by_id +from xmodule.modulestore.django import modulestore + +from django.conf import settings +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + help = "Compute grades for all students in a course, and store result in DB.\n" + help += "Usage: compute_grades course_id_or_dir \n" + help += " course_id_or_dir: either course_id or course_dir\n" + + def handle(self, *args, **options): + + print "args = ", args + + course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' + + if len(args)>0: + course_id = args[0] + + try: + course = get_course_by_id(course_id) + except Exception as err: + if course_id in modulestore().courses: + course = modulestore().courses[course_id] + else: + print "-----------------------------------------------------------------------------" + print "Sorry, cannot find course %s" % course_id + print "Please provide a course ID or course data directory name, eg content-mit-801rq" + return + + print "-----------------------------------------------------------------------------" + print "Computing grades for %s" % (course.id) + + offline_grade_calculation(course.id) + + + + diff --git a/lms/djangoapps/instructor/offline_gradecalc.py b/lms/djangoapps/instructor/offline_gradecalc.py new file mode 100644 index 0000000000..7c102805b4 --- /dev/null +++ b/lms/djangoapps/instructor/offline_gradecalc.py @@ -0,0 +1,103 @@ +# ======== Offline calculation of grades ============================================================================= +# +# Computing grades of a large number of students can take a long time. These routines allow grades to +# be computed offline, by a batch process (eg cronjob). +# +# The grades are stored in the OfflineComputedGrade table of the courseware model. + +import json +import logging +import time + +import courseware.models + +from collections import namedtuple +from json import JSONEncoder +from courseware import grades, models +from courseware.courses import get_course_by_id +from django.contrib.auth.models import User, Group + + +class MyEncoder(JSONEncoder): + + def _iterencode(self, obj, markers=None): + if isinstance(obj, tuple) and hasattr(obj, '_asdict'): + gen = self._iterencode_dict(obj._asdict(), markers) + else: + gen = JSONEncoder._iterencode(self, obj, markers) + for chunk in gen: + yield chunk + + +def offline_grade_calculation(course_id): + ''' + Compute grades for all students for a specified course, and save results to the DB. + ''' + + tstart = time.time() + enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username') + + enc = MyEncoder() + + class DummyRequest(object): + META = {} + def __init__(self): + return + def get_host(self): + return 'edx.mit.edu' + def is_secure(self): + return False + + request = DummyRequest() + + print "%d enrolled students" % len(enrolled_students) + course = get_course_by_id(course_id) + + for student in enrolled_students: + gradeset = grades.grade(student, request, course, keep_raw_scores=True) + gs = enc.encode(gradeset) + ocg, created = models.OfflineComputedGrade.objects.get_or_create(user=student, course_id=course_id) + ocg.gradeset = gs + ocg.save() + print "%s done" % student # print statement used because this is run by a management command + + tend = time.time() + dt = tend - tstart + + ocgl = models.OfflineComputedGradeLog(course_id=course_id, seconds=dt, nstudents=len(enrolled_students)) + ocgl.save() + print ocgl + print "All Done!" + + +def offline_grades_available(course_id): + ''' + Returns False if no offline grades available for specified course. + Otherwise returns latest log field entry about the available pre-computed grades. + ''' + ocgl = models.OfflineComputedGradeLog.objects.filter(course_id=course_id) + if not ocgl: + return False + return ocgl.latest('created') + + +def student_grades(student, request, course, keep_raw_scores=False, use_offline=False): + ''' + This is the main interface to get grades. It has the same parameters as grades.grade, as well + as use_offline. If use_offline is True then this will look for an offline computed gradeset in the DB. + ''' + + if not use_offline: + return grades.grade(student, request, course, keep_raw_scores=keep_raw_scores) + + try: + ocg = models.OfflineComputedGrade.objects.get(user=student, course_id=course.id) + except models.OfflineComputedGrade.DoesNotExist: + return dict(raw_scores=[], section_breakdown=[], + msg='Error: no offline gradeset available for %s, %s' % (student, course.id)) + + return json.loads(ocg.gradeset) + + + + diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 0ea3cf0435..2e8db884ff 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -32,7 +32,8 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views - +from .grading import StaffGrading +from .offline_gradecalc import student_grades, offline_grades_available log = logging.getLogger(__name__) @@ -103,6 +104,7 @@ def instructor_dashboard(request, course_id): # process actions from form POST action = request.POST.get('action', '') + use_offline = request.POST.get('use_offline_grades',False) if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD']: if 'GIT pull' in action: @@ -134,32 +136,32 @@ def instructor_dashboard(request, course_id): if action == 'Dump list of enrolled students' or action=='List enrolled students': log.debug(action) - datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False) + datatable = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) datatable['title'] = 'List of students enrolled in {0}'.format(course_id) track.views.server_track(request, 'list-students', {}, page='idashboard') elif 'Dump Grades' in action: log.debug(action) - datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True) + datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) datatable['title'] = 'Summary Grades of students enrolled in {0}'.format(course_id) track.views.server_track(request, 'dump-grades', {}, page='idashboard') elif 'Dump all RAW grades' in action: log.debug(action) datatable = get_student_grade_summary_data(request, course, course_id, get_grades=True, - get_raw_scores=True) + get_raw_scores=True, use_offline=use_offline) datatable['title'] = 'Raw Grades of students enrolled in {0}'.format(course_id) track.views.server_track(request, 'dump-grades-raw', {}, page='idashboard') elif 'Download CSV of all student grades' in action: track.views.server_track(request, 'dump-grades-csv', {}, page='idashboard') return return_csv('grades_{0}.csv'.format(course_id), - get_student_grade_summary_data(request, course, course_id)) + get_student_grade_summary_data(request, course, course_id, use_offline=use_offline)) elif 'Download CSV of all RAW grades' in action: track.views.server_track(request, 'dump-grades-csv-raw', {}, page='idashboard') return return_csv('grades_{0}_raw.csv'.format(course_id), - get_student_grade_summary_data(request, course, course_id, get_raw_scores=True)) + get_student_grade_summary_data(request, course, course_id, get_raw_scores=True, use_offline=use_offline)) elif 'Download CSV of answer distributions' in action: track.views.server_track(request, 'dump-answer-dist-csv', {}, page='idashboard') @@ -174,7 +176,7 @@ def instructor_dashboard(request, course_id): elif action=='List assignments available for this course': log.debug(action) - allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) assignments = [[x] for x in allgrades['assignments']] datatable = {'header': ['Assignment Name']} @@ -184,7 +186,7 @@ def instructor_dashboard(request, course_id): msg += 'assignments=
%s
' % assignments elif action=='List enrolled students matching remote gradebook': - stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False) + stud_data = get_student_grade_summary_data(request, course, course_id, get_grades=False, use_offline=use_offline) msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') datatable = {'header': ['Student email', 'Match?']} rg_students = [ x['email'] for x in rg_stud_data['retdata'] ] @@ -202,7 +204,7 @@ def instructor_dashboard(request, course_id): if not aname: msg += "Please enter an assignment name" else: - allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True) + allgrades = get_student_grade_summary_data(request, course, course_id, get_grades=True, use_offline=use_offline) if aname not in allgrades['assignments']: msg += "Invalid assignment name '%s'" % aname else: @@ -401,6 +403,12 @@ def instructor_dashboard(request, course_id): problems = psychoanalyze.problems_with_psychometric_data(course_id) + #---------------------------------------- + # offline grades? + + if use_offline: + msg += "
Grades from %s" % offline_grades_available(course_id) + #---------------------------------------- # context for rendering @@ -416,7 +424,8 @@ def instructor_dashboard(request, course_id): 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), 'djangopid' : os.getpid(), - 'mitx_version' : getattr(settings,'MITX_VERSION_STRING','') + 'mitx_version' : getattr(settings,'MITX_VERSION_STRING',''), + 'offline_grade_log' : offline_grades_available(course_id), } return render_to_response('courseware/instructor_dashboard.html', context) @@ -539,7 +548,7 @@ def _update_forum_role_membership(uname, course, rolename, add_or_remove): return msg -def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False): +def get_student_grade_summary_data(request, course, course_id, get_grades=True, get_raw_scores=False, use_offline=False): ''' Return data arrays with student identity and grades for specified course. @@ -563,7 +572,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, assignments = [] if get_grades and enrolled_students.count() > 0: # just to construct the header - gradeset = grades.grade(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores) + gradeset = student_grades(enrolled_students[0], request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline) # log.debug('student {0} gradeset {1}'.format(enrolled_students[0], gradeset)) if get_raw_scores: assignments += [score.section for score in gradeset['raw_scores']] @@ -582,20 +591,22 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, datarow.append('') if get_grades: - gradeset = grades.grade(student, request, course, keep_raw_scores=get_raw_scores) + gradeset = student_grades(student, request, course, keep_raw_scores=get_raw_scores, use_offline=use_offline) log.debug('student={0}, gradeset={1}'.format(student,gradeset)) if get_raw_scores: - student_grades = [score.earned for score in gradeset['raw_scores']] + # TODO (ichuang) encode Score as dict instead of as list, so score[0] -> score['earned'] + sgrades = [(getattr(score,'earned','') or score[0]) for score in gradeset['raw_scores']] else: - student_grades = [x['percent'] for x in gradeset['section_breakdown']] - datarow += student_grades - student.grades = student_grades # store in student object + sgrades = [x['percent'] for x in gradeset['section_breakdown']] + datarow += sgrades + student.grades = sgrades # store in student object data.append(datarow) datatable['data'] = data return datatable - +#----------------------------------------------------------------------------- +# Staff grading @@ -616,7 +627,7 @@ def gradebook(request, course_id): student_info = [{'username': student.username, 'id': student.id, 'email': student.email, - 'grade_summary': grades.grade(student, request, course), + 'grade_summary': student_grades(student, request, course), 'realname': student.profile.name, } for student in enrolled_students] @@ -639,6 +650,10 @@ def grade_summary(request, course_id): return render_to_response('courseware/grade_summary.html', context) +#----------------------------------------------------------------------------- +# enrollment + + def _do_enroll_students(course, course_id, students, overload=False): """Do the actual work of enrolling multiple students, presented as a string of emails separated by commas or returns""" @@ -731,6 +746,9 @@ def enroll_students(request, course_id): 'debug': new_students}) +#----------------------------------------------------------------------------- +# answer distribution + def get_answers_distribution(request, course_id): """ Get the distribution of answers for all graded problems in the course. diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index b2ec220484..235505fc29 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -71,6 +71,12 @@ function goto( mode) ##----------------------------------------------------------------------------- %if modeflag.get('Grades'): + + %if offline_grade_log: +

Pre-computed grades ${offline_grade_log} available: Use? +

+ %endif +

Gradebook

From 16b91cf73277e900ad746deba1ba25bd0a027f34 Mon Sep 17 00:00:00 2001 From: ichuang Date: Mon, 7 Jan 2013 04:35:55 +0000 Subject: [PATCH 006/125] remove spurious old migration file --- .../migrations/0021_remove_askbot.py.old | 157 ------------------ 1 file changed, 157 deletions(-) delete mode 100644 common/djangoapps/student/migrations/0021_remove_askbot.py.old diff --git a/common/djangoapps/student/migrations/0021_remove_askbot.py.old b/common/djangoapps/student/migrations/0021_remove_askbot.py.old deleted file mode 100644 index 89f7208f40..0000000000 --- a/common/djangoapps/student/migrations/0021_remove_askbot.py.old +++ /dev/null @@ -1,157 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - -ASKBOT_AUTH_USER_COLUMNS = ( - 'website', - 'about', - 'gold', - 'email_isvalid', - 'real_name', - 'location', - 'reputation', - 'gravatar', - 'bronze', - 'last_seen', - 'silver', - 'questions_per_page', - 'new_response_count', - 'seen_response_count', -) - - -class Migration(SchemaMigration): - - def forwards(self, orm): - "Kill the askbot" - # For MySQL, we're batching the alters together for performance reasons - if db.backend_name == 'mysql': - drops = ["drop `{0}`".format(col) for col in ASKBOT_AUTH_USER_COLUMNS] - statement = "alter table `auth_user` {0};".format(", ".join(drops)) - db.execute(statement) - else: - for column in ASKBOT_AUTH_USER_COLUMNS: - db.delete_column('auth_user', column) - - def backwards(self, orm): - raise RuntimeError("Cannot reverse this migration: there's no going back to Askbot.") - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'student.courseenrollment': { - 'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, - 'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), - 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) - }, - 'student.pendingemailchange': { - 'Meta': {'object_name': 'PendingEmailChange'}, - 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'student.pendingnamechange': { - 'Meta': {'object_name': 'PendingNameChange'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), - 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'student.registration': { - 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, - 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'student.testcenteruser': { - 'Meta': {'object_name': 'TestCenterUser'}, - 'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}), - 'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), - 'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), - 'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}), - 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), - 'client_candidate_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), - 'company_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), - 'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), - 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), - 'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}), - 'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}), - 'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), - 'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}), - 'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}), - 'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), - 'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), - 'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}), - 'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), - 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}), - 'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}) - }, - 'student.userprofile': { - 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, - 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), - 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), - 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), - 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), - 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) - }, - 'student.usertestgroup': { - 'Meta': {'object_name': 'UserTestGroup'}, - 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), - 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) - } - } - - complete_apps = ['student'] From e5c958082bff84c79bde4e979828796813335d72 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 5 Jan 2013 21:08:35 +0000 Subject: [PATCH 007/125] list/add/delete instructors on instructor dashboard --- lms/djangoapps/courseware/access.py | 14 ++++-- lms/djangoapps/instructor/views.py | 50 +++++++++++++++++-- .../courseware/instructor_dashboard.html | 10 ++++ 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index b58f8d5470..60fcbff3a3 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -171,13 +171,19 @@ def _has_access_course_desc(user, course, action): return _dispatch(checkers, action, user, course) + def _get_access_group_name_course_desc(course, action): ''' - Return name of group which gives staff access to course. Only understands action = 'staff' + Return name of group which gives staff access to course. Only understands action = 'staff' and 'instructor' ''' - if not action=='staff': - return [] - return _course_staff_group_name(course.location) + if action=='staff': + return _course_staff_group_name(course.location) + elif action=='instructor': + return _course_instructor_group_name(course.location) + + return [] + + def _has_access_error_desc(user, descriptor, action): """ diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 2e8db884ff..2e8b9fd6bc 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -94,11 +94,17 @@ def instructor_dashboard(request, course_id): return response def get_staff_group(course): - staffgrp = get_access_group_name(course, 'staff') + return get_group(course, 'staff') + + def get_instructor_group(course): + return get_group(course, 'instructor') + + def get_group(course, groupname): + grpname = get_access_group_name(course, groupname) try: - group = Group.objects.get(name=staffgrp) + group = Group.objects.get(name=grpname) except Group.DoesNotExist: - group = Group(name=staffgrp) # create the group + group = Group(name=grpname) # create the group group.save() return group @@ -239,6 +245,16 @@ def instructor_dashboard(request, course_id): datatable['title'] = 'List of Staff in course {0}'.format(course_id) track.views.server_track(request, 'list-staff', {}, page='idashboard') + elif 'List course instructors' in action: + group = get_instructor_group(course) + msg += 'Instructor group = {0}'.format(group.name) + log.debug('instructor grp={0}'.format(group.name)) + uset = group.user_set.all() + datatable = {'header': ['Username', 'Full name']} + datatable['data'] = [[x.username, x.profile.name] for x in uset] + datatable['title'] = 'List of Instructors in course {0}'.format(course_id) + track.views.server_track(request, 'list-instructors', {}, page='idashboard') + elif action == 'Add course staff': uname = request.POST['staffuser'] try: @@ -253,6 +269,20 @@ def instructor_dashboard(request, course_id): user.groups.add(group) track.views.server_track(request, 'add-staff {0}'.format(user), {}, page='idashboard') + elif action == 'Add instructor': + uname = request.POST['instructor'] + try: + user = User.objects.get(username=uname) + except User.DoesNotExist: + msg += 'Error: unknown username "{0}"'.format(uname) + user = None + if user is not None: + group = get_instructor_group(course) + msg += 'Added {0} to instructor group = {1}'.format(user, group.name) + log.debug('staffgrp={0}'.format(group.name)) + user.groups.add(group) + track.views.server_track(request, 'add-instructor {0}'.format(user), {}, page='idashboard') + elif action == 'Remove course staff': uname = request.POST['staffuser'] try: @@ -267,6 +297,20 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-staff {0}'.format(user), {}, page='idashboard') + elif action == 'Remove instructor': + uname = request.POST['instructor'] + try: + user = User.objects.get(username=uname) + except User.DoesNotExist: + msg += 'Error: unknown username "{0}"'.format(uname) + user = None + if user is not None: + group = get_instructor_group(course) + msg += 'Removed {0} from instructor group = {1}'.format(user, group.name) + log.debug('instructorgrp={0}'.format(group.name)) + user.groups.remove(group) + track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard') + #---------------------------------------- # forum administration diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 235505fc29..7f1912cd45 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -173,6 +173,16 @@ function goto( mode)
%endif + %if admin_access: +
+

+ +

+ + +


+ %endif + %if settings.MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] and admin_access:

From ec94f7328ce77d61329c0b477da52ae8a5037893 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:13:05 -0500 Subject: [PATCH 008/125] remove try...except workaround for circular import bug fixed by https://github.com/MITx/mitx/issues/1260 --- lms/djangoapps/courseware/access.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index 60fcbff3a3..26f9fcdfd3 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -13,12 +13,7 @@ from xmodule.modulestore import Location from xmodule.timeparse import parse_time from xmodule.x_module import XModule, XModuleDescriptor -# student.models imports Role, which imports courseware.access ; use a try, to break the circular import -try: - from student.models import CourseEnrollmentAllowed -except Exception as err: - CourseEnrollmentAllowed = None - +from student.models import CourseEnrollmentAllowed DEBUG_ACCESS = False From 9853c6323f5c3f7d6b65dce6126e647275d56e8b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 10 Jan 2013 15:19:38 -0500 Subject: [PATCH 009/125] Remove circular dependencies that connect student.models and django_comment_client Includes removal of "from django_comment_client.models import Role" from common/djangoapps/student/models.py Conflicts: common/djangoapps/student/models.py --- common/djangoapps/student/models.py | 11 ----------- lms/djangoapps/django_comment_client/models.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index d3254532bc..d9ce790ebe 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -49,7 +49,6 @@ from django.db.models.signals import post_save from django.dispatch import receiver import comment_client as cc -from django_comment_client.models import Role log = logging.getLogger(__name__) @@ -280,16 +279,6 @@ class CourseEnrollmentAllowed(models.Model): return "[CourseEnrollmentAllowed] %s: %s (%s)" % (self.email, self.course_id, self.created) -@receiver(post_save, sender=CourseEnrollment) -def assign_default_role(sender, instance, **kwargs): - if instance.user.is_staff: - role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0] - else: - role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0] - - logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) - instance.user.roles.add(role) - #cache_relation(User.profile) #### Helper methods for use from python manage.py shell. diff --git a/lms/djangoapps/django_comment_client/models.py b/lms/djangoapps/django_comment_client/models.py index 628ac21a4a..a6a2c23603 100644 --- a/lms/djangoapps/django_comment_client/models.py +++ b/lms/djangoapps/django_comment_client/models.py @@ -2,6 +2,10 @@ import logging from django.db import models from django.contrib.auth.models import User +from django.dispatch import receiver +from django.db.models.signals import post_save + +from student.models import CourseEnrollment from courseware.courses import get_course_by_id @@ -45,3 +49,14 @@ class Permission(models.Model): def __unicode__(self): return self.name + + +@receiver(post_save, sender=CourseEnrollment) +def assign_default_role(sender, instance, **kwargs): + if instance.user.is_staff: + role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0] + else: + role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0] + + logging.info("assign_default_role: adding %s as %s" % (instance.user, role)) + instance.user.roles.add(role) From 5cc88ec1adbc04b4a6459c8acd90209bb2e5adc9 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:25:29 -0500 Subject: [PATCH 010/125] example course_id for compute_grades management command --- .../instructor/management/commands/compute_grades.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/management/commands/compute_grades.py b/lms/djangoapps/instructor/management/commands/compute_grades.py index 717bfd5802..462833ba3c 100644 --- a/lms/djangoapps/instructor/management/commands/compute_grades.py +++ b/lms/djangoapps/instructor/management/commands/compute_grades.py @@ -19,15 +19,17 @@ class Command(BaseCommand): help = "Compute grades for all students in a course, and store result in DB.\n" help += "Usage: compute_grades course_id_or_dir \n" help += " course_id_or_dir: either course_id or course_dir\n" + help += 'Example course_id: MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' def handle(self, *args, **options): print "args = ", args - course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' - if len(args)>0: course_id = args[0] + else: + print self.help + return try: course = get_course_by_id(course_id) From 37f848949d80a0dc8aea45e6d8f7f560bbb61628 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:29:52 -0500 Subject: [PATCH 011/125] only is_staff users can add/edit/delete course instructors --- lms/djangoapps/instructor/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 2e8b9fd6bc..07dbfacc64 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -245,7 +245,7 @@ def instructor_dashboard(request, course_id): datatable['title'] = 'List of Staff in course {0}'.format(course_id) track.views.server_track(request, 'list-staff', {}, page='idashboard') - elif 'List course instructors' in action: + elif 'List course instructors' in action and request.user.is_staff: group = get_instructor_group(course) msg += 'Instructor group = {0}'.format(group.name) log.debug('instructor grp={0}'.format(group.name)) @@ -269,7 +269,7 @@ def instructor_dashboard(request, course_id): user.groups.add(group) track.views.server_track(request, 'add-staff {0}'.format(user), {}, page='idashboard') - elif action == 'Add instructor': + elif action == 'Add instructor' and request.user.is_staff: uname = request.POST['instructor'] try: user = User.objects.get(username=uname) @@ -297,7 +297,7 @@ def instructor_dashboard(request, course_id): user.groups.remove(group) track.views.server_track(request, 'remove-staff {0}'.format(user), {}, page='idashboard') - elif action == 'Remove instructor': + elif action == 'Remove instructor' and request.user.is_staff: uname = request.POST['instructor'] try: user = User.objects.get(username=uname) From ed96046ad77e609330968834eeee8e4e6a573037 Mon Sep 17 00:00:00 2001 From: ichuang Date: Thu, 10 Jan 2013 23:59:20 -0500 Subject: [PATCH 012/125] add documentation on remote gradebook xserver API --- doc/remote_gradebook.md | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 doc/remote_gradebook.md diff --git a/doc/remote_gradebook.md b/doc/remote_gradebook.md new file mode 100644 index 0000000000..3743e98753 --- /dev/null +++ b/doc/remote_gradebook.md @@ -0,0 +1,47 @@ +Grades can be pushed to a remote gradebook, and course enrollment membership can be pulled from a remote gradebook. This file documents how to setup such a remote gradebook, and what the API should be for writing new remote gradebook "xservers". + +1. Definitions + +An "xserver" is a web-based server that is part of the MITx eco system. There are a number of "xserver" programs, including one which does python code grading, an xqueue server, and graders for other coding languages. + +"Stellar" is the MIT on-campus gradebook system. + +2. Setup + +The remote gradebook xserver should be specified in the lms.envs configuration using + + MITX_FEATURES[REMOTE_GRADEBOOK_URL] + +Each course, in addition, should define the name of the gradebook being used. A class "section" may also be specified. This goes in the policy.json file, eg: + + "remote_gradebook": { + "name" : "STELLAR:/project/mitxdemosite", + "section" : "r01" + }, + +3. The API for the remote gradebook xserver is an almost RESTful service model, which only employs POSTs, to the xserver url, with form data for the fields: + + - submit: get-assignments, get-membership, post-grades, or get-sections + - gradebook: name of gradebook + - user: username of staff person initiating the request (for logging) + - section: (optional) name of section + +The return body content should be a JSON string, of the format {'msg': message, 'data': data}. The message is displayed in the instructor dashboard. + +The data is a list of dicts (associative arrays). Each dict should be key:value. + +## For submit=post-grades: + +A file is also posted, with the field name "datafile". This file is CSV format, with two columns, one being "External email" and the other being the name of the assignment (that column contains the grades for the assignment). + +## For submit=get-assignments + +data keys = "AssignmentName" + +## For submit=get-membership + +data keys = "email", "name", "section" + +## For submit=get-sections + +data keys = "SectionName" From b7ad39a0b96de070776933ed96690ff4d3f9b35b Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 11 Jan 2013 00:09:46 -0500 Subject: [PATCH 013/125] instructor dashboard shouldn't import .grading anymore --- lms/djangoapps/instructor/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 07dbfacc64..3a3380407d 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -32,7 +32,6 @@ from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundErr from xmodule.modulestore.search import path_to_location import track.views -from .grading import StaffGrading from .offline_gradecalc import student_grades, offline_grades_available log = logging.getLogger(__name__) From 4f869ad38258a8664344188c5e20fa8a328311e7 Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 11 Jan 2013 00:10:52 -0500 Subject: [PATCH 014/125] remove spurious comment --- lms/djangoapps/instructor/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 3a3380407d..2d58799efe 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -649,9 +649,6 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, return datatable #----------------------------------------------------------------------------- -# Staff grading - - @cache_control(no_cache=True, no_store=True, must_revalidate=True) def gradebook(request, course_id): From 5dd83e4d5fb2d31d18522be16aebfa3ff4e0a9c6 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 11 Jan 2013 11:10:51 -0500 Subject: [PATCH 015/125] Remove import --- common/djangoapps/student/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 46e316ac0b..cf7bc7696a 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -49,7 +49,6 @@ from django.db.models.signals import post_save from django.dispatch import receiver import comment_client as cc -from django_comment_client.models import Role log = logging.getLogger(__name__) From 833b0c34abc4a93c25abcb0cd29e4734c2e42fc1 Mon Sep 17 00:00:00 2001 From: Jennifer Akana Date: Mon, 14 Jan 2013 19:20:49 -0500 Subject: [PATCH 016/125] added problem weight description --- doc/course_grading.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/doc/course_grading.md b/doc/course_grading.md index 6dce2fa70e..5c668df5d9 100644 --- a/doc/course_grading.md +++ b/doc/course_grading.md @@ -35,6 +35,43 @@ weights of 30, 10, 10, and 10 to the 4 problems, respectively. Note that the default weight of a problem **is not 1.** The default weight of a problem is the module's max_grade. +If weighting is set, each problem is worth the number of points assigned, regardless of the number of responses it contains. + +Consider a Homework section that contains two problems. + + + ... + + +and + + + ... + ... + ... + + + + + + +Without weighting, Problem 1 is worth 25% of the assignment, and Problem 2 is worth 75% of the assignment. + +Weighting for the problems can be set in the policy.json file. + + "problem/problem1": { + "weight": 2 + }, + "problem/problem2": { + "weight": 2 + }, + +With the above weighting, Problems 1 and 2 are each worth 50% of the assignment. + +Please note: When problems have weight, the point value is automatically included in the display name *except* when “weight”: 1.When “weight”: 1, no visual change occurs in the display name, leaving the point value open to interpretation to the student. + + + ## Section Weighting Once each section has a percentage score, we must total those sections into a From c5107d42de7d3cafef4c7f0fec8f7785aa7f50fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 26 Nov 2012 17:38:59 +0200 Subject: [PATCH 017/125] bug --- common/lib/xmodule/setup.py | 1 + common/lib/xmodule/xmodule/gst_module.py | 141 +++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 common/lib/xmodule/xmodule/gst_module.py diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index c867fca228..8deb2a7ce5 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -36,6 +36,7 @@ setup( "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor", + "gst = xmodule.gst_module:GSTDescriptor", ] } ) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py new file mode 100644 index 0000000000..8847a5224c --- /dev/null +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -0,0 +1,141 @@ +""" +GST (Graphical-Slider-Tool) module is ungraded xmodule used by students to +understand functional dependencies +""" + +# import json +import logging + +from lxml import etree + +from xmodule.mako_module import MakoModuleDescriptor +from xmodule.xml_module import XmlDescriptor +from xmodule.x_module import XModule +from xmodule.progress import Progress +from xmodule.exceptions import NotFoundError +from pkg_resources import resource_string +from xmodule.raw_module import RawDescriptor + +# log = logging.getLogger("mitx.common.lib.seq_module") + + +class GSTModule(XModule): + ''' Graphical-Slider-Tool Module + ''' + # js = {'js': [resource_string(__name__, 'js/src/gst/gst.js')]} + # #css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} + # js_module_name = "GST" + + def __init__(self, system, location, definition, descriptor, instance_state=None, + shared_state=None, **kwargs): + """ + pass + # Definition should have.... + # sliders, text, module + + # Sample file: + + # + #

Plot...

+ # + # + # + # + # + # + # + # + # + # """ + # XModule.__init__(self, system, location, definition, descriptor, + # instance_state, shared_state, **kwargs) + # import ipdb; ipdb.set_trace() + # self.rendered = False + + # def get_html(self): + # self.render() + # return self.content + + # def render(self): + # import ipdb; ipdb.set_trace() + # if self.rendered: + # return + # ## Returns a set of all types of all sub-children + # contents = [] + # # import ipdb; ipdb.set_trace() + # for child in self.get_display_items(): + # progress = child.get_progress() + # childinfo = { + # 'gst': child.get_html(), + # 'plot': "\n".join( + # grand_child.display_name.strip() + # for grand_child in child.get_children() + # if 'display_name' in grand_child.metadata + # ), + # # 'progress_status': Progress.to_js_status_str(progress), + # 'progress_detail': Progress.to_js_detail_str(progress), + # 'type': child.get_icon_class(), + # } + # # if childinfo['title']=='': + # # childinfo['title'] = child.metadata.get('display_name','') + # contents.append(childinfo) + + # params = {'items': contents, + # 'element_id': self.location.html_id(), + # 'item_id': self.id, + # 'position': self.position, + # 'tag': self.location.category + # } + + # self.content = self.system.render_template('seq_module.html', params) + # self.rendered = True + + +class GSTDescriptor(RawDescriptor): + mako_template = "widgets/html-edit.html" + module_class = GSTModule + template_dir_name = 'gst' + + # @classmethod + # def definition_from_xml(cls, xml_object, system): + # """ + # Pull out the data into dictionary. + + # Returns: + # { + # 'def1': 'def1-some-html', + # 'def2': 'def2-some-html' + # } + # """ + # import ipdb; ipdb.set_trace() + # children = [] + # for child in xml_object: + # try: + # children.append(system.process_xml(etree.tostring(child)).location.url()) + # except: + # log.exception("Unable to load child when parsing GST. Continuing...") + # continue + # return {'children': children} + + # def definition_to_xml(self, resource_fs): + # '''Return an xml element representing this definition.''' + # import ipdb; ipdb.set_trace() + # xml_object = etree.Element('gst') + + # def add_child(k): + # # child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) + # child_str = child.export_to_xml(resource_fs) + # child_node = etree.fromstring(child_str) + # xml_object.append(child_node) + + # for child in self.get_children(): + # add_child(child) + + # return xml_object + + + # def __init__(self, system, definition, **kwargs): + # '''Render and save the template for this descriptor instance''' + # super(CustomTagDescriptor, self).__init__(system, definition, **kwargs) + # self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file From d0fcaf0ac4f2de72ce696e125905184eab934ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80?= Date: Mon, 26 Nov 2012 18:11:01 +0200 Subject: [PATCH 018/125] next step --- common/lib/xmodule/xmodule/gst_module.py | 71 ++++++++++++------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 8847a5224c..285eeb8fce 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -97,45 +97,46 @@ class GSTDescriptor(RawDescriptor): module_class = GSTModule template_dir_name = 'gst' - # @classmethod - # def definition_from_xml(cls, xml_object, system): - # """ - # Pull out the data into dictionary. + @classmethod + def definition_from_xml(cls, xml_object, system): + """ + Pull out the data into dictionary. - # Returns: - # { - # 'def1': 'def1-some-html', - # 'def2': 'def2-some-html' - # } - # """ - # import ipdb; ipdb.set_trace() - # children = [] - # for child in xml_object: - # try: - # children.append(system.process_xml(etree.tostring(child)).location.url()) - # except: - # log.exception("Unable to load child when parsing GST. Continuing...") - # continue - # return {'children': children} + Returns: + { + 'def1': 'def1-some-html', + 'def2': 'def2-some-html' + } + """ + import ipdb; ipdb.set_trace() + children = [] + for child in xml_object: + try: + children.append(system.process_xml(etree.tostring(child)).location.url()) + except: + log.exception("Unable to load child when parsing GST. Continuing...") + continue + return {'children': children} - # def definition_to_xml(self, resource_fs): - # '''Return an xml element representing this definition.''' - # import ipdb; ipdb.set_trace() - # xml_object = etree.Element('gst') + def definition_to_xml(self, resource_fs): + '''Return an xml element representing this definition.''' + import ipdb; ipdb.set_trace() + xml_object = etree.Element('gst') - # def add_child(k): - # # child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) - # child_str = child.export_to_xml(resource_fs) - # child_node = etree.fromstring(child_str) - # xml_object.append(child_node) + def add_child(k): + # child_str = '<{tag}>{body}'.format(tag=k, body=self.definition[k]) + child_str = child.export_to_xml(resource_fs) + child_node = etree.fromstring(child_str) + xml_object.append(child_node) - # for child in self.get_children(): - # add_child(child) + for child in self.get_children(): + add_child(child) - # return xml_object + return xml_object - # def __init__(self, system, definition, **kwargs): - # '''Render and save the template for this descriptor instance''' - # super(CustomTagDescriptor, self).__init__(system, definition, **kwargs) - # self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file + def __init__(self, system, definition, **kwargs): + '''Render and save the template for this descriptor instance''' + import ipdb; ipdb.set_trace() + super(CustomTagDescriptor, self).__init__(system, definition, **kwargs) + self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file From 884db888f953d651d18a1f4cf5445da436451bdd Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Wed, 28 Nov 2012 16:48:07 +0200 Subject: [PATCH 019/125] teting gst --- common/lib/xmodule/xmodule/gst_module.py | 128 +++++++++++------------ 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 285eeb8fce..f5b2095d95 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -29,70 +29,70 @@ class GSTModule(XModule): def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): """ - pass - # Definition should have.... - # sliders, text, module + Definition should have.... + sliders, text, module - # Sample file: + Sample file: - # - #

Plot...

- # - # - # - # - # - # - # - # - #
- # """ - # XModule.__init__(self, system, location, definition, descriptor, - # instance_state, shared_state, **kwargs) - # import ipdb; ipdb.set_trace() - # self.rendered = False + +

Plot...

+ + + + + + + + +
+ """ + XModule.__init__(self, system, location, definition, descriptor, + instance_state, shared_state, **kwargs) + # import ipdb; ipdb.set_trace() + # self.rendered = False - # def get_html(self): - # self.render() - # return self.content + def get_html(self): + self.render() + return self.content - # def render(self): - # import ipdb; ipdb.set_trace() - # if self.rendered: - # return - # ## Returns a set of all types of all sub-children - # contents = [] - # # import ipdb; ipdb.set_trace() - # for child in self.get_display_items(): - # progress = child.get_progress() - # childinfo = { - # 'gst': child.get_html(), - # 'plot': "\n".join( - # grand_child.display_name.strip() - # for grand_child in child.get_children() - # if 'display_name' in grand_child.metadata - # ), - # # 'progress_status': Progress.to_js_status_str(progress), - # 'progress_detail': Progress.to_js_detail_str(progress), - # 'type': child.get_icon_class(), - # } - # # if childinfo['title']=='': - # # childinfo['title'] = child.metadata.get('display_name','') - # contents.append(childinfo) + def render(self): + # import ipdb; ipdb.set_trace() + # if self.rendered: + return + ## Returns a set of all types of all sub-children + # contents = [] + # # import ipdb; ipdb.set_trace() + # for child in self.get_display_items(): + # progress = child.get_progress() + # childinfo = { + # 'gst': child.get_html(), + # 'plot': "\n".join( + # grand_child.display_name.strip() + # for grand_child in child.get_children() + # if 'display_name' in grand_child.metadata + # ), + # # 'progress_status': Progress.to_js_status_str(progress), + # 'progress_detail': Progress.to_js_detail_str(progress), + # 'type': child.get_icon_class(), + # } + # # if childinfo['title']=='': + # # childinfo['title'] = child.metadata.get('display_name','') + # contents.append(childinfo) - # params = {'items': contents, - # 'element_id': self.location.html_id(), - # 'item_id': self.id, - # 'position': self.position, - # 'tag': self.location.category - # } - - # self.content = self.system.render_template('seq_module.html', params) - # self.rendered = True + # params = {'items': contents, + # 'element_id': self.location.html_id(), + # 'item_id': self.id, + # 'position': self.position, + # 'tag': self.location.category + # } + params= {} + self.content = self.system.render_template('gst_module.html', params) + # self.rendered = True -class GSTDescriptor(RawDescriptor): +# class GSTDescriptor(RawDescriptor): +class GSTDescriptor(MakoModuleDescriptor, XmlDescriptor): mako_template = "widgets/html-edit.html" module_class = GSTModule template_dir_name = 'gst' @@ -108,7 +108,7 @@ class GSTDescriptor(RawDescriptor): 'def2': 'def2-some-html' } """ - import ipdb; ipdb.set_trace() + # import ipdb; ipdb.set_trace() children = [] for child in xml_object: try: @@ -120,7 +120,7 @@ class GSTDescriptor(RawDescriptor): def definition_to_xml(self, resource_fs): '''Return an xml element representing this definition.''' - import ipdb; ipdb.set_trace() + # import ipdb; ipdb.set_trace() xml_object = etree.Element('gst') def add_child(k): @@ -135,8 +135,8 @@ class GSTDescriptor(RawDescriptor): return xml_object - def __init__(self, system, definition, **kwargs): - '''Render and save the template for this descriptor instance''' - import ipdb; ipdb.set_trace() - super(CustomTagDescriptor, self).__init__(system, definition, **kwargs) - self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file + # def __init__(self, system, definition, **kwargs): + # '''Render and save the template for this descriptor instance''' + # # import ipdb; ipdb.set_trace() + # super(GSTDescriptor, self).__init__(system, definition, **kwargs) + # self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file From 07cb76a7d99e822b4dbc9c140c6ad929c2bbbbc9 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Thu, 29 Nov 2012 16:43:33 +0200 Subject: [PATCH 020/125] first working version of gst module --- common/lib/xmodule/setup.py | 2 +- common/lib/xmodule/xmodule/gst_module.py | 150 +++++++++++------------ 2 files changed, 73 insertions(+), 79 deletions(-) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 8deb2a7ce5..86636ef05a 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -36,7 +36,7 @@ setup( "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor", - "gst = xmodule.gst_module:GSTDescriptor", + "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor", ] } ) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index f5b2095d95..d90bc46e6e 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -1,6 +1,6 @@ """ -GST (Graphical-Slider-Tool) module is ungraded xmodule used by students to -understand functional dependencies +Graphical slider tool module is ungraded xmodule used by students to +understand functional dependencies. """ # import json @@ -15,11 +15,12 @@ from xmodule.progress import Progress from xmodule.exceptions import NotFoundError from pkg_resources import resource_string from xmodule.raw_module import RawDescriptor +from xmodule.stringify import stringify_children # log = logging.getLogger("mitx.common.lib.seq_module") -class GSTModule(XModule): +class GraphicalSliderToolModule(XModule): ''' Graphical-Slider-Tool Module ''' # js = {'js': [resource_string(__name__, 'js/src/gst/gst.js')]} @@ -34,92 +35,92 @@ class GSTModule(XModule): Sample file: - -

Plot...

- - - - - - - - -
+ + + + + Graphic slider tool html. Can include + 'number', 'slider' and plot tags. They will be replaced + by proper number, slider and plot widgets. + + + + + + + + + + + + + -10, 1, 10 + + 1 + 1 + + + + + + """ XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) - # import ipdb; ipdb.set_trace() - # self.rendered = False def get_html(self): - self.render() + params = { + 'main_html': self.definition['render'].strip(), + 'element_id': self.location.html_id(), + 'element_class': self.location.category + } + self.content = (self.system.render_template( + 'graphical_slider_tool.html', params)) return self.content - def render(self): - # import ipdb; ipdb.set_trace() - # if self.rendered: - return - ## Returns a set of all types of all sub-children - # contents = [] - # # import ipdb; ipdb.set_trace() - # for child in self.get_display_items(): - # progress = child.get_progress() - # childinfo = { - # 'gst': child.get_html(), - # 'plot': "\n".join( - # grand_child.display_name.strip() - # for grand_child in child.get_children() - # if 'display_name' in grand_child.metadata - # ), - # # 'progress_status': Progress.to_js_status_str(progress), - # 'progress_detail': Progress.to_js_detail_str(progress), - # 'type': child.get_icon_class(), - # } - # # if childinfo['title']=='': - # # childinfo['title'] = child.metadata.get('display_name','') - # contents.append(childinfo) - # params = {'items': contents, - # 'element_id': self.location.html_id(), - # 'item_id': self.id, - # 'position': self.position, - # 'tag': self.location.category - # } - params= {} - self.content = self.system.render_template('gst_module.html', params) - # self.rendered = True - - -# class GSTDescriptor(RawDescriptor): -class GSTDescriptor(MakoModuleDescriptor, XmlDescriptor): - mako_template = "widgets/html-edit.html" - module_class = GSTModule - template_dir_name = 'gst' +class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): + module_class = GraphicalSliderToolModule + template_dir_name = 'graphical_slider_tool' @classmethod def definition_from_xml(cls, xml_object, system): """ Pull out the data into dictionary. + Args: + xml_object: xml from file. + Returns: - { - 'def1': 'def1-some-html', - 'def2': 'def2-some-html' - } + dict """ - # import ipdb; ipdb.set_trace() - children = [] - for child in xml_object: - try: - children.append(system.process_xml(etree.tostring(child)).location.url()) - except: - log.exception("Unable to load child when parsing GST. Continuing...") - continue - return {'children': children} + # check for presense of required tags in xml + expected_children_level_0 = ['render', 'configuration'] + for child in expected_children_level_0: + if len(xml_object.xpath(child)) != 1: + raise ValueError("Self a\ssessment definition must include \ + exactly one '{0}' tag".format(child)) + expected_children_level_1 = ['plot'] + for child in expected_children_level_1: + if len(xml_object.xpath('configuration')[0].xpath(child)) != 1: + raise ValueError("Self a\ssessment definition must include \ + exactly one '{0}' tag".format(child)) + # finished + + def parse(k): + """Assumes that xml_object has child k""" + return stringify_children(xml_object.xpath(k)[0]) + + return { + 'render': parse('render'), + 'configuration': xml_object.xpath('configuration')[0], + } def definition_to_xml(self, resource_fs): - '''Return an xml element representing this definition.''' + '''Return an xml element representing this definition. + Not implemented''' # import ipdb; ipdb.set_trace() xml_object = etree.Element('gst') @@ -133,10 +134,3 @@ class GSTDescriptor(MakoModuleDescriptor, XmlDescriptor): add_child(child) return xml_object - - - # def __init__(self, system, definition, **kwargs): - # '''Render and save the template for this descriptor instance''' - # # import ipdb; ipdb.set_trace() - # super(GSTDescriptor, self).__init__(system, definition, **kwargs) - # self.rendered_html = self.render_template(system, definition['data']) \ No newline at end of file From 54c222a0153073126a74cec3acb050a3e578e24e Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Thu, 29 Nov 2012 16:44:10 +0200 Subject: [PATCH 021/125] template for gst --- lms/templates/graphical_slider_tool.html | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 lms/templates/graphical_slider_tool.html diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html new file mode 100644 index 0000000000..cb484d9e6c --- /dev/null +++ b/lms/templates/graphical_slider_tool.html @@ -0,0 +1,3 @@ +
+${main_html} +
From 169d4b123ca9f803ab7b5381a4c4450ece1719a8 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Fri, 30 Nov 2012 14:55:00 +0200 Subject: [PATCH 022/125] updates --- common/lib/xmodule/xmodule/gst_module.py | 50 +++++++++++++++++++----- lms/templates/graphical_slider_tool.html | 14 ++++++- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index d90bc46e6e..e98b7b6c90 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -3,21 +3,19 @@ Graphical slider tool module is ungraded xmodule used by students to understand functional dependencies. """ -# import json +import json import logging - from lxml import etree +import xmltodict +import re from xmodule.mako_module import MakoModuleDescriptor from xmodule.xml_module import XmlDescriptor from xmodule.x_module import XModule -from xmodule.progress import Progress -from xmodule.exceptions import NotFoundError -from pkg_resources import resource_string -from xmodule.raw_module import RawDescriptor from xmodule.stringify import stringify_children -# log = logging.getLogger("mitx.common.lib.seq_module") + +log = logging.getLogger("mitx.common.lib.gst_module") class GraphicalSliderToolModule(XModule): @@ -71,15 +69,49 @@ class GraphicalSliderToolModule(XModule): instance_state, shared_state, **kwargs) def get_html(self): + self.get_configuration() + gst_html = self.substitute_controls(self.definition['render'].strip()) + params = { - 'main_html': self.definition['render'].strip(), + 'gst_html': gst_html, 'element_id': self.location.html_id(), - 'element_class': self.location.category + 'element_class': self.location.category, + 'configuration_json': self.configuration_json } self.content = (self.system.render_template( 'graphical_slider_tool.html', params)) + # import ipdb; ipdb.set_trace() return self.content + def substitute_controls(self, html_string): + """ Substitue control element via their divs. + Simple variant: slider and plot controls are not inside any tag. + """ + plot_div = '
\ + This is plot
' + html_string.replace('$plot$', plot_div) + vars = [x['@var'] for x in json.loads(self.configuration_json)['root']['sliders']['slider']] + for var in vars: + m = re.match('$slider\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', '')) + if m: + # Note: we subtract 15 to compensate for the size of the dot on the screen. + # (is a 30x30 image--lms/static/green-pointer.png). + (self.gx, self.gy) = [int(x) - 15 for x in m.groups()] + html.replace('$slider' + ' ' + x['@var']) + return html_string + + def get_configuration(self): + """Parse self.definition['configuration'] and transfer it to javascript + via json. + """ + # root added for interface compatibility with xmltodict.parse + self.configuration_json = json.dumps( + xmltodict.parse('' + + stringify_children(self.definition['configuration']) + + '')) + return self.configuration_json + class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): module_class = GraphicalSliderToolModule diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index cb484d9e6c..7d6cc292c5 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -1,3 +1,13 @@ -
-${main_html} +
+ +
+ + +${gst_html} + + +{# widgests
#} + +
From 69d0156c36c9316b365aa6128d09c737f3b78177 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Fri, 30 Nov 2012 17:17:32 +0200 Subject: [PATCH 023/125] slider, plot and input contrlos are transferred from xml to html substituted to proper div elements --- common/lib/xmodule/xmodule/gst_module.py | 50 ++++++++++++++++++------ lms/templates/graphical_slider_tool.html | 7 +--- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index e98b7b6c90..2d65b768ec 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -70,12 +70,14 @@ class GraphicalSliderToolModule(XModule): def get_html(self): self.get_configuration() + self.html_id = self.location.html_id() + self.html_class = self.location.category gst_html = self.substitute_controls(self.definition['render'].strip()) - + params = { 'gst_html': gst_html, - 'element_id': self.location.html_id(), - 'element_class': self.location.category, + 'element_id': self.html_id, + 'element_class': self.html_class, 'configuration_json': self.configuration_json } self.content = (self.system.render_template( @@ -87,18 +89,44 @@ class GraphicalSliderToolModule(XModule): """ Substitue control element via their divs. Simple variant: slider and plot controls are not inside any tag. """ + #substitute plot plot_div = '
\ This is plot
' - html_string.replace('$plot$', plot_div) - vars = [x['@var'] for x in json.loads(self.configuration_json)['root']['sliders']['slider']] + html_string = html_string.replace('$plot$', plot_div) + + # substitute sliders + sliders = json.loads(self.configuration_json)['root']['sliders']['slider'] + if type(sliders) == dict: + sliders = [sliders] + vars = [x['@var'] for x in sliders] + + slider_div = '
This is slider
' + for var in vars: - m = re.match('$slider\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', '')) - if m: - # Note: we subtract 15 to compensate for the size of the dot on the screen. - # (is a 30x30 image--lms/static/green-pointer.png). - (self.gx, self.gy) = [int(x) - 15 for x in m.groups()] - html.replace('$slider' + ' ' + x['@var']) + html_string = re.sub(r'\$slider\s+' + var + r'\$', + slider_div.format(element_class=self.html_class, + element_id=self.html_id, + var=var), + html_string, flags=re.IGNORECASE | re.UNICODE) + + # substitute numbers + inputs = json.loads(self.configuration_json)['root']['inputs']['input'] + if type(inputs) == dict: + inputs = [inputs] + vars = [x['@var'] for x in inputs] + + input_div = '
This is input
' + + for var in vars: + html_string = re.sub(r'\$input\s+' + var + r'\$', + input_div.format(element_class=self.html_class, + element_id=self.html_id, + var=var), + html_string, flags=re.IGNORECASE | re.UNICODE) + # import ipdb; ipdb.set_trace() return html_string def get_configuration(self): diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index 7d6cc292c5..920e53cab3 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -1,13 +1,10 @@
+
+data-json="${configuration_json}">
${gst_html} - -{# widgests
#} - -
From 30df77b9b24131c9e5960a81838379844c11727e Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Fri, 30 Nov 2012 17:21:30 +0200 Subject: [PATCH 024/125] added xmltodict to dependencies --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index a3e1e3e6e5..08cfe57e2e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,3 +56,4 @@ dogstatsd-python==0.2.1 sphinx==1.1.3 Shapely==1.2.16 ipython==0.13.1 +xmltodict==0.4.1 From da4a676513e201290ce02bdd36b90725163068f6 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Mon, 3 Dec 2012 15:08:37 +0200 Subject: [PATCH 025/125] working xmodule - GsT --- common/lib/xmodule/xmodule/gst_module.py | 68 +++++++++++++------ .../js/src/graphical_slider_tool/gst.js | 17 +++++ lms/templates/graphical_slider_tool.html | 12 ++++ 3 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 2d65b768ec..60c03dec10 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -13,6 +13,7 @@ from xmodule.mako_module import MakoModuleDescriptor from xmodule.xml_module import XmlDescriptor from xmodule.x_module import XModule from xmodule.stringify import stringify_children +from pkg_resources import resource_string log = logging.getLogger("mitx.common.lib.gst_module") @@ -21,9 +22,8 @@ log = logging.getLogger("mitx.common.lib.gst_module") class GraphicalSliderToolModule(XModule): ''' Graphical-Slider-Tool Module ''' - # js = {'js': [resource_string(__name__, 'js/src/gst/gst.js')]} - # #css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} - # js_module_name = "GST" + js = {'js': [resource_string(__name__, 'js/src/graphical_slider_tool/gst.js')]} + js_module_name = "GraphicalSliderTool" def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): @@ -37,30 +37,55 @@ class GraphicalSliderToolModule(XModule): - Graphic slider tool html. Can include - 'number', 'slider' and plot tags. They will be replaced - by proper number, slider and plot widgets. +

Graphic slider tool html.

+

Can include 'input', 'slider' and 'plot' tags. + They will be replaced by proper number, slider and plot + widgets.

+ For example: $slider a$, second $slider b$, + number $input a$, and, plot: + $plot$ + +
- + + + - - - + + + + - - - - -10, 1, 10 - - 1 - 1 + + + + + + + + -10, 10 + + 60 + + -9, 1, 9 + -9, 1, 9 + + + + +
@@ -73,12 +98,13 @@ class GraphicalSliderToolModule(XModule): self.html_id = self.location.html_id() self.html_class = self.location.category gst_html = self.substitute_controls(self.definition['render'].strip()) - + # import ipdb; ipdb.set_trace() params = { 'gst_html': gst_html, 'element_id': self.html_id, 'element_class': self.html_class, - 'configuration_json': self.configuration_json + 'configuration_json': self.configuration_json, + 'plot_code': self.definition['plot_code'] } self.content = (self.system.render_template( 'graphical_slider_tool.html', params)) @@ -157,11 +183,12 @@ class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): dict """ # check for presense of required tags in xml - expected_children_level_0 = ['render', 'configuration'] + expected_children_level_0 = ['render', 'configuration', 'plot_code'] for child in expected_children_level_0: if len(xml_object.xpath(child)) != 1: raise ValueError("Self a\ssessment definition must include \ exactly one '{0}' tag".format(child)) + expected_children_level_1 = ['plot'] for child in expected_children_level_1: if len(xml_object.xpath('configuration')[0].xpath(child)) != 1: @@ -176,6 +203,7 @@ class GraphicalSliderToolDescriptor(MakoModuleDescriptor, XmlDescriptor): return { 'render': parse('render'), 'configuration': xml_object.xpath('configuration')[0], + 'plot_code': parse('plot_code'), } def definition_to_xml(self, resource_fs): diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js new file mode 100644 index 0000000000..03778ea437 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js @@ -0,0 +1,17 @@ +// Graphical Slider Tool module + +(function() { + this.GraphicalSliderTool = (function() { + function GST(el) { + console.log(el); + // element is : + //
+ } + // console.log('in GST'); + return GST; + + })(); +}).call(this); +// this=window, after call +// window['Graphical_Slider_Tool'] is available. \ No newline at end of file diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index 920e53cab3..fc5052893a 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -4,7 +4,19 @@
+ +
+ ${gst_html}
+ + + From 643ac69a57af84733318fd06675a3f1126661ff2 Mon Sep 17 00:00:00 2001 From: valera-rozuvan Date: Wed, 5 Dec 2012 16:56:31 +0200 Subject: [PATCH 026/125] Separating JS from the html for GST. --- common/static/js/graphical_slider_tool/main.js | 1 + lms/templates/graphical_slider_tool.html | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 common/static/js/graphical_slider_tool/main.js diff --git a/common/static/js/graphical_slider_tool/main.js b/common/static/js/graphical_slider_tool/main.js new file mode 100644 index 0000000000..bb624d5888 --- /dev/null +++ b/common/static/js/graphical_slider_tool/main.js @@ -0,0 +1 @@ +alert('Hello, world!'); diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index fc5052893a..97fca83ff4 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -14,9 +14,9 @@ ${gst_html}
- From 0913e9ef69c6f12f5d87195c54d683de040bc82c Mon Sep 17 00:00:00 2001 From: valera-rozuvan Date: Wed, 5 Dec 2012 17:16:57 +0200 Subject: [PATCH 027/125] Work on GST. --- .../js/src/graphical_slider_tool/module.js | 15 ++++ .../js/graphical_slider_tool/gst_module.js | 15 ++++ .../static/js/graphical_slider_tool/main.js | 76 ++++++++++++++++++- lms/envs/common.py | 1 + lms/templates/graphical_slider_tool.html | 26 ++----- 5 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js create mode 100644 common/static/js/graphical_slider_tool/gst_module.js diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js new file mode 100644 index 0000000000..c4661b5e44 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js @@ -0,0 +1,15 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define([], function () { + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/static/js/graphical_slider_tool/gst_module.js b/common/static/js/graphical_slider_tool/gst_module.js new file mode 100644 index 0000000000..c4661b5e44 --- /dev/null +++ b/common/static/js/graphical_slider_tool/gst_module.js @@ -0,0 +1,15 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define([], function () { + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/static/js/graphical_slider_tool/main.js b/common/static/js/graphical_slider_tool/main.js index bb624d5888..da36d9c9d6 100644 --- a/common/static/js/graphical_slider_tool/main.js +++ b/common/static/js/graphical_slider_tool/main.js @@ -1 +1,75 @@ -alert('Hello, world!'); +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +// For documentation please check: +// http://requirejs.org/docs/api.html +requirejs.config({ + // Because require.js is included as a simple From 5990fa2ef5e382465f2d042efbe5a44eba5a4762 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Fri, 7 Dec 2012 17:25:24 +0200 Subject: [PATCH 028/125] Integrated RequireJS with xmodule for GST. --- common/lib/xmodule/xmodule/gst_module.py | 14 ++++++- .../js/src/graphical_slider_tool/gst.js | 37 ++++++++++--------- .../js/src/graphical_slider_tool/gst_main.js | 17 +++++++++ .../{module.js => mod1.js} | 3 +- .../js/src/graphical_slider_tool/mod2.js | 16 ++++++++ .../js/src/graphical_slider_tool/mod3.js | 19 ++++++++++ .../js/src/graphical_slider_tool/mod4.js | 16 ++++++++ .../js/src/graphical_slider_tool/mod5.js | 16 ++++++++ 8 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js rename common/lib/xmodule/xmodule/js/src/graphical_slider_tool/{module.js => mod1.js} (88%) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 60c03dec10..f89cb0f990 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -22,7 +22,19 @@ log = logging.getLogger("mitx.common.lib.gst_module") class GraphicalSliderToolModule(XModule): ''' Graphical-Slider-Tool Module ''' - js = {'js': [resource_string(__name__, 'js/src/graphical_slider_tool/gst.js')]} + + js = { + 'js': [ + resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod1.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod2.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod3.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod4.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/mod5.js'), + + resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') + ] + } js_module_name = "GraphicalSliderTool" def __init__(self, system, location, definition, descriptor, instance_state=None, diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js index 03778ea437..1434d05f70 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst.js @@ -1,17 +1,20 @@ -// Graphical Slider Tool module - -(function() { - this.GraphicalSliderTool = (function() { - function GST(el) { - console.log(el); - // element is : - //
- } - // console.log('in GST'); - return GST; - - })(); -}).call(this); -// this=window, after call -// window['Graphical_Slider_Tool'] is available. \ No newline at end of file +/* + * We will add a function that will be called for all GraphicalSliderTool + * xmodule module instances. It must be available globally by design of + * xmodule. + */ +window.GraphicalSliderTool = function (el) { + // All the work will be performed by the GstMain module. We will get access + // to it, and all it's dependencies, via Require JS. Currently Require JS + // is namespaced and is available via a global object RequireJS. + RequireJS.require(['GstMain'], function (GstMain) { + // The GstMain module expects the DOM ID of a Graphical Slider Tool + // element. Since we are given a
element which might in + // theory contain multiple graphical_slider_tool
elements (each + // with a unique DOM ID), we will iterate over all children, and for + // each match, we will call GstMain module. + $(el).children('.graphical_slider_tool').each(function (index, value) { + GstMain($(value).attr('id')); + }); + }); +}; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js new file mode 100644 index 0000000000..66f98eddf7 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -0,0 +1,17 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('GstMain', ['mod1', 'mod2', 'mod3', 'mod4'], function (mod1, mod2, mod3, mod4) { + return GstMain; + + function GstMain(gstId) { + console.log('The DOM ID of the current GST element is ' + gstId); + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js similarity index 88% rename from common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js rename to common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js index c4661b5e44..44674b96d3 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/module.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js @@ -2,7 +2,8 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define([], function () { +define('mod1', [], function () { + console.log('we are in the mod1 callback'); return { 'module_status': 'OK' }; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js new file mode 100644 index 0000000000..9c26bb1dfe --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js @@ -0,0 +1,16 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod2', [], function () { + console.log('we are in the mod2 callback'); + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js new file mode 100644 index 0000000000..21961f3611 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js @@ -0,0 +1,19 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod3', ['mod5'], function (mod5) { + console.log('we are in the mod3 callback'); + + console.log('mod5 status: [' + mod5.module_status + '].'); + + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js new file mode 100644 index 0000000000..0edf809155 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js @@ -0,0 +1,16 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod4', [], function () { + console.log('we are in the mod4 callback'); + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js new file mode 100644 index 0000000000..5e843ac468 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js @@ -0,0 +1,16 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('mod5', [], function () { + console.log('we are in the mod5 callback'); + return { + 'module_status': 'OK' + }; +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) From 080e96fdc481b95f9a50348c368872baacc17a12 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 08:28:34 +0200 Subject: [PATCH 029/125] Work in progress on GST. --- common/lib/xmodule/xmodule/gst_module.py | 14 +- .../{mod3.js => general_methods.js} | 12 +- .../js/src/graphical_slider_tool/gst_main.js | 12 +- .../js/src/graphical_slider_tool/logme.js | 54 ++++++ .../js/src/graphical_slider_tool/mod1.js | 16 -- .../js/src/graphical_slider_tool/mod2.js | 16 -- .../js/src/graphical_slider_tool/mod4.js | 16 -- .../js/src/graphical_slider_tool/sliders.js | 142 +++++++++++++++ .../js/src/graphical_slider_tool/state.js | 165 ++++++++++++++++++ lms/templates/graphical_slider_tool.html | 16 +- 10 files changed, 395 insertions(+), 68 deletions(-) rename common/lib/xmodule/xmodule/js/src/graphical_slider_tool/{mod3.js => general_methods.js} (65%) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index f89cb0f990..3d7b8a9f02 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -26,10 +26,10 @@ class GraphicalSliderToolModule(XModule): js = { 'js': [ resource_string(__name__, 'js/src/graphical_slider_tool/gst_main.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod1.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod2.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod3.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod4.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/state.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), resource_string(__name__, 'js/src/graphical_slider_tool/mod5.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') @@ -128,7 +128,7 @@ class GraphicalSliderToolModule(XModule): Simple variant: slider and plot controls are not inside any tag. """ #substitute plot - plot_div = '
\ This is plot
' html_string = html_string.replace('$plot$', plot_div) @@ -139,7 +139,7 @@ class GraphicalSliderToolModule(XModule): sliders = [sliders] vars = [x['@var'] for x in sliders] - slider_div = '
This is input
' for var in vars: diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js similarity index 65% rename from common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js rename to common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js index 21961f3611..9cdd4fff0f 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod3.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/general_methods.js @@ -2,12 +2,16 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('mod3', ['mod5'], function (mod5) { - console.log('we are in the mod3 callback'); - - console.log('mod5 status: [' + mod5.module_status + '].'); +define('GeneralMethods', [], function () { + if (!String.prototype.trim) { + // http://blog.stevenlevithan.com/archives/faster-trim-javascript + String.prototype.trim = function trim(str) { + return str.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + }; + } return { + 'module_name': 'GeneralMethods', 'module_status': 'OK' }; }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 66f98eddf7..9f2c4c356d 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -2,11 +2,19 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('GstMain', ['mod1', 'mod2', 'mod3', 'mod4'], function (mod1, mod2, mod3, mod4) { +define('GstMain', ['State', 'logme', 'GeneralMethods', 'Sliders'], function (State, logme, GeneralMethods, Sliders) { + logme(GeneralMethods); + return GstMain; function GstMain(gstId) { - console.log('The DOM ID of the current GST element is ' + gstId); + var config, state; + + config = JSON.parse($('#' + gstId + '_json').html()).root; + + state = State(gstId, config); + + Sliders(gstId, config, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js new file mode 100644 index 0000000000..c045757044 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/logme.js @@ -0,0 +1,54 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('logme', [], function () { + var debugMode; + + // debugMode can be one of the following: + // + // true - All messages passed to logme will be written to the internal + // browser console. + // false - Suppress all output to the internal browser console. + // + // Obviously, if anywhere there is a direct console.log() call, we can't do + // anything about it. That's why use logme() - it will allow to turn off + // the output of debug information with a single change to a variable. + debugMode = true; + + return logme; + + /* + * function: logme + * + * A helper function that provides logging facilities. We don't want + * to call console.log() directly, because sometimes it is not supported + * by the browser. Also when everything is routed through this function. + * the logging output can be easily turned off. + * + * logme() supports multiple parameters. Each parameter will be passed to + * console.log() function separately. + * + */ + function logme() { + var i; + + if ( + (typeof debugMode === 'undefined') || + (debugMode !== true) || + (typeof window.console === 'undefined') + ) { + return; + } + + for (i = 0; i < arguments.length; i++) { + window.console.log(arguments[i]); + } + } // End-of: function logme +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js deleted file mode 100644 index 44674b96d3..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod1.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod1', [], function () { - console.log('we are in the mod1 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js deleted file mode 100644 index 9c26bb1dfe..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod2.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod2', [], function () { - console.log('we are in the mod2 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js deleted file mode 100644 index 0edf809155..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod4.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod4', [], function () { - console.log('we are in the mod4 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js new file mode 100644 index 0000000000..6ef53bdbeb --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -0,0 +1,142 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('Sliders', ['logme'], function (logme) { + return Sliders; + + function Sliders(gstId, config, state) { + logme('We are inside Sliders function.'); + + logme('gstId: ' + gstId); + logme(config); + logme(state); + + // We will go through all of the sliders. For each one, we will make a + // jQuery UI slider for it, attach "on change" events, and set it's + // state - initial value, max, and min parameters. + if ((typeof config.sliders !== 'undefined') && + (typeof config.sliders.slider !== 'undefined')) { + if ($.isArray(config.sliders.slider)) { + // config.sliders.slider is an array + + for (c1 = 0; c1 < config.sliders.slider.length; c1++) { + createSlider(config.sliders.slider[c1]); + } + } else if ($.isPlainObject(config.sliders.slider)) { + // config.sliders.slider is an object + createSlider(config.sliders.slider); + } + } + + function createSlider(obj) { + var constName, constValue, rangeBlobs, valueMin, valueMax, + sliderDiv, sliderWidth; + + // The name of the constant is obj['@var']. Multiple sliders and/or + // inputs can represent the same constant - therefore we will get + // the most recent const value from the state object. The range is + // a string composed of 3 blobs, separated by commas. The first + // blob is the min value for the slider, the third blob is the max + // value for the slider. + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + constValue = state.getConstValue(constName); + if (constValue === undefined) { + constValue = 0; + } + + if (typeof obj['@range'] !== 'string') { + valueMin = constValue - 10; + valueMax = constValue + 10; + } else { + rangeBlobs = obj['@range'].split(','); + + // We must have gotten exactly 3 blobs (pieces) from the split. + if (rangeBlobs.length !== 3) { + valueMin = constValue - 10; + valueMax = constValue + 10; + } else { + // Get the first blob from the split string. + valueMin = parseFloat(rangeBlobs[0]); + + if (isNaN(valueMin) === true) { + valueMin = constValue - 10; + } + + // Get the third blob from the split string. + valueMax = parseFloat(rangeBlobs[2]); + + if (isNaN(valueMax) === true) { + valueMax = constValue + 10; + } + + // Logically, the min, value, and max should make sense. + // I.e. we will make sure that: + // + // min <= value <= max + // + // If this is not the case, we will set some defaults. + if ((valueMin > valueMax) || + (valueMin > constValue) || + (valueMax < constValue)) { + valueMin = constValue - 10; + valueMax = constValue + 10; + } + } + } + + sliderDiv = $('#' + gstId + '_slider_' + constName); + + // If a corresponding slider DIV for this constant does not exist, + // do not do anything. + if (sliderDiv.length === 0) { + return; + } + + // The default slider width. + sliderWidth = 400; + + logme('width: 0'); + logme(obj['@width']); + if (typeof obj['@width'] === 'string') { + logme('width: 1'); + if (isNaN(parseInt(obj['@width'], 10)) === false) { + logme('width: 2'); + sliderWidth = parseInt(obj['@width'], 10); + } + } + + // Set the new width to the slider. + sliderDiv.width(sliderWidth); + + // Create a jQuery UI slider from the current DIV. We will set + // starting parameters, and will also attach a handler to update + // the state on the change event. + sliderDiv.slider({ + 'min': valueMin, + 'max': valueMax, + 'value': constValue, + + 'change': sliderOnChange + }); + + return; + + function sliderOnChange(event, ui) { + state.setConstValue(constName, ui.value); + } + } + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js new file mode 100644 index 0000000000..17c8721a73 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -0,0 +1,165 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('State', ['logme'], function (logme) { + // Since there will be (can be) multiple GST on a page, and each will have + // a separate state, we will create a factory constructor function. The + // constructor will expect the ID of the DIV with the GST contents, and the + // configuration object (parsed from a JSON string). It will return and + // object containing methods to set and get the private state properties. + + // This module defines and returns a factory constructor. + return State; + + /* + * function: State + * + * + */ + function State(gstId, config) { + var constants, c1; + + constants = {}; + + // We must go through all of the input, and slider elements and + // retrieve all of the available constants. These will be added to an + // object as it's properties. + // + // First we will go through all of the inputs. + if ((typeof config.inputs !== 'undefined') && + (typeof config.inputs.input !== 'undefined')) { + if ($.isArray(config.inputs.input)) { + // config.inputs.input is an array + + for (c1 = 0; c1 < config.inputs.input.length; c1++) { + addConstFromInput(config.inputs.input[c1]); + } + } else if ($.isPlainObject(config.inputs.input)) { + // config.inputs.input is an object + addConstFromInput(config.inputs.input); + } + } + + // Now we will go through all of the sliders. + if ((typeof config.sliders !== 'undefined') && + (typeof config.sliders.slider !== 'undefined')) { + if ($.isArray(config.sliders.slider)) { + // config.sliders.slider is an array + + for (c1 = 0; c1 < config.sliders.slider.length; c1++) { + addConstFromSlider(config.sliders.slider[c1]); + } + } else if ($.isPlainObject(config.sliders.slider)) { + // config.sliders.slider is an object + addConstFromSlider(config.sliders.slider); + } + } + + logme(constants); + + // The constructor will return an object with methods to operate on + // it's private properties. + return { + 'getConstValue': getConstValue, + 'setConstValue': setConstValue + }; + + function getConstValue(constName) { + if (constants.hasOwnProperty(constName) === false) { + // If the name of the constant is not tracked by state, return an + // 'undefined' value. + return; + } + + return constants[constName]; + } + + function setConstValue(constName, constValue) { + if (constants.hasOwnProperty(constName) === false) { + // If the name of the constant is not tracked by state, return an + // 'undefined' value. + return; + } + + if (isNaN(parseFloat(constValue)) === true) { + // We are interested only in valid float values. + return; + } + + constants[constName] = parseFloat(constValue); + + logme('From setConstValue: new value for "' + constName + '" is ' + constValue); + } + + function addConstFromInput(obj) { + var constName, constValue; + + // The name of the constant is obj['@var']. The value (initial) of + // the constant is obj['@initial']. I have taken the word 'initial' + // into brackets, because multiple inputs and/or sliders can + // represent the state of a single constant. + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + if (typeof obj['@initial'] === 'undefined') { + constValue = 0; + } else { + constValue = parseFloat(obj['@initial']); + + if (isNaN(constValue) === true) { + constValue = 0; + } + } + + constants[constName] = constValue; + } + + function addConstFromSlider(obj) { + var constName, constValue, rangeBlobs; + + // The name of the constant is obj['@var']. The value (initial) of + // the constant is the second blob of the 'range' parameter of the + // slider which is obj['@range']. Multiple sliders and/or inputs + // can represent the same constant - therefore 'initial' is in + // brackets. The range is a string composed of 3 blobs, separated + // by commas. + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + if (typeof obj['@range'] !== 'string') { + constValue = 0; + } else { + rangeBlobs = obj['@range'].split(','); + + // We must have gotten exactly 3 blobs (pieces) from the split. + if (rangeBlobs.length !== 3) { + constValue = 0; + } else { + // Get the second blob from the split string. + constValue = parseFloat(rangeBlobs[1]); + + if (isNaN(constValue) === true) { + constValue = 0; + } + } + } + + constants[constName] = constValue; + } + } // End-of: function State +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/lms/templates/graphical_slider_tool.html b/lms/templates/graphical_slider_tool.html index d6cffc67e2..17d2bae5e9 100644 --- a/lms/templates/graphical_slider_tool.html +++ b/lms/templates/graphical_slider_tool.html @@ -1,12 +1,14 @@
- -
+ + - -
+ + - + ${gst_html}
From ce7a01dd26b0f407487b4c78a27bad559b9699ae Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 09:08:36 +0200 Subject: [PATCH 030/125] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 2 +- .../js/src/graphical_slider_tool/graph.js | 62 +++++++++++++++++++ .../js/src/graphical_slider_tool/gst_main.js | 7 ++- .../js/src/graphical_slider_tool/mod5.js | 16 ----- .../js/src/graphical_slider_tool/state.js | 8 +++ 5 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js delete mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 3d7b8a9f02..633f5e9406 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -30,7 +30,7 @@ class GraphicalSliderToolModule(XModule): resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), - resource_string(__name__, 'js/src/graphical_slider_tool/mod5.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') ] diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js new file mode 100644 index 0000000000..2aa19cfc02 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -0,0 +1,62 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('Graph', ['logme'], function (logme) { + + return Graph; + + function Graph(gstId, state) { + var plotDiv, data; + logme('We are inside Graph module.', gstId, state); + + plotDiv = $('#' + gstId + '_plot'); + + if (plotDiv.length === 0) { + return; + } + + plotDiv.width(300); + plotDiv.height(300); + + plotDiv.bind('update_plot', function (event, forGstId) { + if (forGstId !== gstId) { + logme('update_plot event not for current ID'); + } + + logme('redrawing plot'); + + generateData(); + updatePlot(); + }); + + generateData(); + updatePlot(); + + return; + + function generateData() { + var a, b, c1; + + a = state.getConstValue('a'); + b = state.getConstValue('b'); + + data = []; + data.push([]); + + for (c1 = 0; c1 < 30; c1++) { + data[0].push([c1, a * c1 * (c1 + a)* (c1 - b) + b * c1 * (c1 + b * a)]); + } + } + + function updatePlot() { + $.plot(plotDiv, data, {xaxis: {min: 0, max: 30}}); + } + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 9f2c4c356d..68ef73e441 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -2,7 +2,10 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('GstMain', ['State', 'logme', 'GeneralMethods', 'Sliders'], function (State, logme, GeneralMethods, Sliders) { +define( + 'GstMain', + ['State', 'logme', 'GeneralMethods', 'Sliders', 'Graph'], + function (State, logme, GeneralMethods, Sliders, Graph) { logme(GeneralMethods); return GstMain; @@ -15,6 +18,8 @@ define('GstMain', ['State', 'logme', 'GeneralMethods', 'Sliders'], function (Sta state = State(gstId, config); Sliders(gstId, config, state); + + Graph(gstId, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js deleted file mode 100644 index 5e843ac468..0000000000 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/mod5.js +++ /dev/null @@ -1,16 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define('mod5', [], function () { - console.log('we are in the mod5 callback'); - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index 17c8721a73..ffd618c51b 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -76,6 +76,8 @@ define('State', ['logme'], function (logme) { } function setConstValue(constName, constValue) { + var plotDiv; + if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an // 'undefined' value. @@ -90,6 +92,12 @@ define('State', ['logme'], function (logme) { constants[constName] = parseFloat(constValue); logme('From setConstValue: new value for "' + constName + '" is ' + constValue); + + plotDiv = $('#' + gstId + '_plot'); + + if (plotDiv.length === 1) { + plotDiv.trigger('update_plot', [gstId]); + } } function addConstFromInput(obj) { From 7de575a84bf5f1915b5a58c0e0646e8c7f6e99e7 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 09:26:29 +0200 Subject: [PATCH 031/125] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 18 ++++++++---------- .../js/src/graphical_slider_tool/state.js | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 2aa19cfc02..fbd1f96da1 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -19,22 +19,20 @@ define('Graph', ['logme'], function (logme) { plotDiv.width(300); plotDiv.height(300); - plotDiv.bind('update_plot', function (event, forGstId) { - if (forGstId !== gstId) { - logme('update_plot event not for current ID'); - } - - logme('redrawing plot'); - - generateData(); - updatePlot(); - }); + state.bindUpdatePlotEvent(plotDiv, onUpdatePlot); generateData(); updatePlot(); return; + function onUpdatePlot(event) { + logme('redrawing plot'); + + generateData(); + updatePlot(); + } + function generateData() { var a, b, c1; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index ffd618c51b..735c100344 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -18,7 +18,7 @@ define('State', ['logme'], function (logme) { * */ function State(gstId, config) { - var constants, c1; + var constants, c1, plotDiv; constants = {}; @@ -62,9 +62,16 @@ define('State', ['logme'], function (logme) { // it's private properties. return { 'getConstValue': getConstValue, - 'setConstValue': setConstValue + 'setConstValue': setConstValue, + 'bindUpdatePlotEvent': bindUpdatePlotEvent }; + function bindUpdatePlotEvent(newPlotDiv, callback) { + plotDiv = newPlotDiv; + + plotDiv.bind('update_plot', callback); + } + function getConstValue(constName) { if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an @@ -76,8 +83,6 @@ define('State', ['logme'], function (logme) { } function setConstValue(constName, constValue) { - var plotDiv; - if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an // 'undefined' value. @@ -93,10 +98,8 @@ define('State', ['logme'], function (logme) { logme('From setConstValue: new value for "' + constName + '" is ' + constValue); - plotDiv = $('#' + gstId + '_plot'); - - if (plotDiv.length === 1) { - plotDiv.trigger('update_plot', [gstId]); + if (plotDiv !== undefined) { + plotDiv.trigger('update_plot'); } } From 32c70a524c3a3dca1cb1939b6417f9677ee8809b Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 11:37:55 +0200 Subject: [PATCH 032/125] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 95 ++++++++++++++++--- .../js/src/graphical_slider_tool/gst_main.js | 7 +- .../js/src/graphical_slider_tool/sliders.js | 12 +-- .../js/src/graphical_slider_tool/state.js | 34 +++++-- 4 files changed, 113 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index fbd1f96da1..762789cbf5 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -2,13 +2,12 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Graph', ['logme'], function (logme) { +define('Graph', [], function () { return Graph; - function Graph(gstId, state) { - var plotDiv, data; - logme('We are inside Graph module.', gstId, state); + function Graph(gstId, config, state) { + var plotDiv, dataSets, functions; plotDiv = $('#' + gstId + '_plot'); @@ -21,34 +20,102 @@ define('Graph', ['logme'], function (logme) { state.bindUpdatePlotEvent(plotDiv, onUpdatePlot); + createFunctions(); + generateData(); updatePlot(); return; - function onUpdatePlot(event) { - logme('redrawing plot'); + function createFunctions() { + functions = []; + if (typeof config.plot['function'] === 'undefined') { + return; + } + + if (typeof config.plot['function'] === 'string') { + addFunction(config.plot['function']); + } else if ($.isPlainObject(config.plot['function']) === true) { + + } else if ($.isArray(config.plot['function'])) { + + } + + return; + + function addFunction(funcString, color, line, dot, label, style, point_size) { + var newFunctionObject, func, constNames; + + if (typeof funcString !== 'string') { + return; + } + + newFunctionObject = {}; + + constNames = state.getAllConstantNames(); + + // The 'x' is always one of the function parameters. + constNames.push('x'); + + // Must make sure that the function body also gets passed to + // the Function cosntructor. + constNames.push(funcString); + + func = Function.apply(null, constNames); + newFunctionObject['func'] = func; + + if (typeof color === 'string') { + newFunctionObject['color'] = color; + } + + if (typeof line === 'boolean') { + newFunctionObject['line'] = line; + } + + if (typeof dot === 'boolean') { + newFunctionObject['dot'] = dot; + } + + if (typeof label === 'string') { + newFunctionObject['label'] = label; + } + + functions.push(newFunctionObject); + } + } + + function onUpdatePlot(event) { generateData(); updatePlot(); } function generateData() { - var a, b, c1; + var c0, c1, datapoints, constValues, x, y; - a = state.getConstValue('a'); - b = state.getConstValue('b'); + constValues = state.getAllConstantValues(); - data = []; - data.push([]); + dataSets = []; - for (c1 = 0; c1 < 30; c1++) { - data[0].push([c1, a * c1 * (c1 + a)* (c1 - b) + b * c1 * (c1 + b * a)]); + for (c0 = 0; c0 < functions.length; c0 += 1) { + datapoints = []; + + for (c1 = 0; c1 < 30; c1 += 0.1) { + x = c1; + // Push the 'x' variable to the end of the parameter array. + constValues.push(x); + y = functions[c0].func.apply(window, constValues); + constValues.pop(); + + datapoints.push([x, y]); + } + + dataSets.push(datapoints); } } function updatePlot() { - $.plot(plotDiv, data, {xaxis: {min: 0, max: 30}}); + $.plot(plotDiv, dataSets); } } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 68ef73e441..71de12b423 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -4,9 +4,8 @@ define( 'GstMain', - ['State', 'logme', 'GeneralMethods', 'Sliders', 'Graph'], - function (State, logme, GeneralMethods, Sliders, Graph) { - logme(GeneralMethods); + ['State', 'GeneralMethods', 'Sliders', 'Graph'], + function (State, GeneralMethods, Sliders, Graph) { return GstMain; @@ -19,7 +18,7 @@ define( Sliders(gstId, config, state); - Graph(gstId, state); + Graph(gstId, config, state); } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 6ef53bdbeb..e871e9f035 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -2,16 +2,10 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Sliders', ['logme'], function (logme) { +define('Sliders', [], function () { return Sliders; function Sliders(gstId, config, state) { - logme('We are inside Sliders function.'); - - logme('gstId: ' + gstId); - logme(config); - logme(state); - // We will go through all of the sliders. For each one, we will make a // jQuery UI slider for it, attach "on change" events, and set it's // state - initial value, max, and min parameters. @@ -102,12 +96,8 @@ define('Sliders', ['logme'], function (logme) { // The default slider width. sliderWidth = 400; - logme('width: 0'); - logme(obj['@width']); if (typeof obj['@width'] === 'string') { - logme('width: 1'); if (isNaN(parseInt(obj['@width'], 10)) === false) { - logme('width: 2'); sliderWidth = parseInt(obj['@width'], 10); } } diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index 735c100344..d632429c9b 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -2,7 +2,7 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('State', ['logme'], function (logme) { +define('State', [], function () { // Since there will be (can be) multiple GST on a page, and each will have // a separate state, we will create a factory constructor function. The // constructor will expect the ID of the DIV with the GST contents, and the @@ -56,16 +56,40 @@ define('State', ['logme'], function (logme) { } } - logme(constants); - // The constructor will return an object with methods to operate on // it's private properties. return { 'getConstValue': getConstValue, 'setConstValue': setConstValue, - 'bindUpdatePlotEvent': bindUpdatePlotEvent + 'bindUpdatePlotEvent': bindUpdatePlotEvent, + 'getAllConstantNames': getAllConstantNames, + 'getAllConstantValues': getAllConstantValues }; + function getAllConstantNames() { + var constName, allConstNames; + + allConstNames = []; + + for (constName in constants) { + allConstNames.push(constName); + } + + return allConstNames; + } + + function getAllConstantValues() { + var constName, allConstValues; + + allConstValues = []; + + for (constName in constants) { + allConstValues.push(constants[constName]); + } + + return allConstValues; + } + function bindUpdatePlotEvent(newPlotDiv, callback) { plotDiv = newPlotDiv; @@ -96,8 +120,6 @@ define('State', ['logme'], function (logme) { constants[constName] = parseFloat(constValue); - logme('From setConstValue: new value for "' + constName + '" is ' + constValue); - if (plotDiv !== undefined) { plotDiv.trigger('update_plot'); } From ae03090f3c980ce5b7c7ee5862f31acc86b08773 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 12:43:01 +0200 Subject: [PATCH 033/125] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 5 ++--- .../xmodule/xmodule/js/src/graphical_slider_tool/graph.js | 8 +++++++- .../xmodule/js/src/graphical_slider_tool/sliders.js | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 633f5e9406..c07c0670d7 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -129,8 +129,7 @@ class GraphicalSliderToolModule(XModule): """ #substitute plot plot_div = '
\ - This is plot
' + style="width: 600px; height: 600px; padding: 0px; position: relative;">This is plot
' html_string = html_string.replace('$plot$', plot_div) # substitute sliders @@ -140,7 +139,7 @@ class GraphicalSliderToolModule(XModule): vars = [x['@var'] for x in sliders] slider_div = '
This is slider
' + data-var="{var}">' for var in vars: html_string = re.sub(r'\$slider\s+' + var + r'\$', diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 762789cbf5..991cb0a26e 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -28,6 +28,8 @@ define('Graph', [], function () { return; function createFunctions() { + var c1; + functions = []; if (typeof config.plot['function'] === 'undefined') { @@ -39,7 +41,11 @@ define('Graph', [], function () { } else if ($.isPlainObject(config.plot['function']) === true) { } else if ($.isArray(config.plot['function'])) { - + for (c1 = 0; c1 < config.plot['function'].length; c1++) { + if (typeof config.plot['function'][c1] === 'string') { + addFunction(config.plot['function'][c1]); + } + } } return; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index e871e9f035..226f53d696 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -112,6 +112,7 @@ define('Sliders', [], function () { 'min': valueMin, 'max': valueMax, 'value': constValue, + 'step': 0.01, 'change': sliderOnChange }); From 3e9d325a9f166901a2e24bc2dfe3942173f892c8 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 14:07:06 +0200 Subject: [PATCH 034/125] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 5 +- .../js/src/graphical_slider_tool/gst_main.js | 5 +- .../js/src/graphical_slider_tool/inputs.js | 70 +++++++++++++++++++ .../js/src/graphical_slider_tool/sliders.js | 1 + .../js/src/graphical_slider_tool/state.js | 7 ++ 5 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index c07c0670d7..9e9273bc25 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -30,6 +30,7 @@ class GraphicalSliderToolModule(XModule): resource_string(__name__, 'js/src/graphical_slider_tool/logme.js'), resource_string(__name__, 'js/src/graphical_slider_tool/general_methods.js'), resource_string(__name__, 'js/src/graphical_slider_tool/sliders.js'), + resource_string(__name__, 'js/src/graphical_slider_tool/inputs.js'), resource_string(__name__, 'js/src/graphical_slider_tool/graph.js'), resource_string(__name__, 'js/src/graphical_slider_tool/gst.js') @@ -154,8 +155,8 @@ class GraphicalSliderToolModule(XModule): inputs = [inputs] vars = [x['@var'] for x in inputs] - input_div = '
This is input
' + input_div = '' for var in vars: html_string = re.sub(r'\$input\s+' + var + r'\$', diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 71de12b423..47881b66c6 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -4,8 +4,8 @@ define( 'GstMain', - ['State', 'GeneralMethods', 'Sliders', 'Graph'], - function (State, GeneralMethods, Sliders, Graph) { + ['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph'], + function (State, GeneralMethods, Sliders, Inputs, Graph) { return GstMain; @@ -17,6 +17,7 @@ define( state = State(gstId, config); Sliders(gstId, config, state); + Inputs(gstId, config, state); Graph(gstId, config, state); } diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js new file mode 100644 index 0000000000..5b9f1f87c2 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -0,0 +1,70 @@ +// Wrapper for RequireJS. It will make the standard requirejs(), require(), and +// define() functions from Require JS available inside the anonymous function. +(function (requirejs, require, define) { + +define('Inputs', ['logme'], function (logme) { + return Inputs; + + function Inputs(gstId, config, state) { + logme('Inside "Inputs" module.'); + logme(gstId, config, state); + + // We will go thorugh all of the inputs, and those that have a valid + // '@var' property will be added to the page as a HTML text input + // element. + if ((typeof config.inputs !== 'undefined') && + (typeof config.inputs.input !== 'undefined')) { + if ($.isArray(config.inputs.input)) { + // config.inputs.input is an array + + for (c1 = 0; c1 < config.inputs.input.length; c1++) { + createInput(config.inputs.input[c1]); + } + } else if ($.isPlainObject(config.inputs.input)) { + // config.inputs.input is an object + createInput(config.inputs.input); + } + } + + function createInput(obj) { + var constName, constValue, inputDiv, textInputDiv; + + if (typeof obj['@var'] === 'undefined') { + return; + } + + constName = obj['@var']; + + constValue = state.getConstValue(constName); + if (constValue === undefined) { + constValue = 0; + } + + inputDiv = $('#' + gstId + '_input_' + constName); + + if (inputDiv.length === 0) { + return; + } + + textInputDiv = $(''); + textInputDiv.width(50); + + textInputDiv.appendTo(inputDiv); + textInputDiv.val(constValue); + + textInputDiv.bind('change', inputOnChange); + + return; + + function inputOnChange(event) { + state.setConstValue(constName, $(this).val()); + } + } + } +}); + +// End of wrapper for RequireJS. As you can see, we are passing +// namespaced Require JS variables to an anonymous function. Within +// it, you can use the standard requirejs(), require(), and define() +// functions as if they were in the global namespace. +}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 226f53d696..51bd2c8b12 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -104,6 +104,7 @@ define('Sliders', [], function () { // Set the new width to the slider. sliderDiv.width(sliderWidth); + sliderDiv.css('display', 'inline-block'); // Create a jQuery UI slider from the current DIV. We will set // starting parameters, and will also attach a handler to update diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js index d632429c9b..88951f0e9d 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/state.js @@ -107,6 +107,8 @@ define('State', [], function () { } function setConstValue(constName, constValue) { + var inputDiv; + if (constants.hasOwnProperty(constName) === false) { // If the name of the constant is not tracked by state, return an // 'undefined' value. @@ -123,6 +125,11 @@ define('State', [], function () { if (plotDiv !== undefined) { plotDiv.trigger('update_plot'); } + + inputDiv = $('#' + gstId + '_input_' + constName).children('input'); + if (inputDiv.length !== 0) { + inputDiv.val(constValue); + } } function addConstFromInput(obj) { From b08b25b98388e4f42f501c930f8d7a4490fca960 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 16:11:29 +0200 Subject: [PATCH 035/125] GST work in progress. --- common/lib/xmodule/xmodule/gst_module.py | 4 +-- .../js/src/graphical_slider_tool/inputs.js | 27 ++++++++++++------- .../js/src/graphical_slider_tool/sliders.js | 20 ++++++++------ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py index 9e9273bc25..61a883fbf8 100644 --- a/common/lib/xmodule/xmodule/gst_module.py +++ b/common/lib/xmodule/xmodule/gst_module.py @@ -139,8 +139,8 @@ class GraphicalSliderToolModule(XModule): sliders = [sliders] vars = [x['@var'] for x in sliders] - slider_div = '
' + slider_div = '' for var in vars: html_string = re.sub(r'\$slider\s+' + var + r'\$', diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js index 5b9f1f87c2..d7f64328e0 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -27,9 +27,9 @@ define('Inputs', ['logme'], function (logme) { } function createInput(obj) { - var constName, constValue, inputDiv, textInputDiv; + var constName, constValue, spanEl, inputEl; - if (typeof obj['@var'] === 'undefined') { + if (typeof obj['@var'] !== 'string') { return; } @@ -40,19 +40,28 @@ define('Inputs', ['logme'], function (logme) { constValue = 0; } - inputDiv = $('#' + gstId + '_input_' + constName); + spanEl = $('#' + gstId + '_input_' + constName); - if (inputDiv.length === 0) { + if (spanEl.length === 0) { return; } - textInputDiv = $(''); - textInputDiv.width(50); + inputEl = $(''); - textInputDiv.appendTo(inputDiv); - textInputDiv.val(constValue); + // inputEl.width(50); + inputEl.val(constValue); + inputEl.bind('change', inputOnChange); + inputEl.button().css({ + 'font': 'inherit', + 'color': 'inherit', + 'text-align': 'left', + 'outline': 'none', + 'cursor': 'text', + 'height': '15px', + 'width': '50px' + }); - textInputDiv.bind('change', inputOnChange); + inputEl.appendTo(spanEl); return; diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 51bd2c8b12..3db0c3e67c 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -25,7 +25,7 @@ define('Sliders', [], function () { function createSlider(obj) { var constName, constValue, rangeBlobs, valueMin, valueMax, - sliderDiv, sliderWidth; + spanEl, sliderEl, sliderWidth; // The name of the constant is obj['@var']. Multiple sliders and/or // inputs can represent the same constant - therefore we will get @@ -34,7 +34,7 @@ define('Sliders', [], function () { // blob is the min value for the slider, the third blob is the max // value for the slider. - if (typeof obj['@var'] === 'undefined') { + if (typeof obj['@var'] !== 'string') { return; } @@ -85,14 +85,16 @@ define('Sliders', [], function () { } } - sliderDiv = $('#' + gstId + '_slider_' + constName); + spanEl = $('#' + gstId + '_slider_' + constName); // If a corresponding slider DIV for this constant does not exist, // do not do anything. - if (sliderDiv.length === 0) { + if (spanEl.length === 0) { return; } + sliderEl = $('
'); + // The default slider width. sliderWidth = 400; @@ -103,21 +105,23 @@ define('Sliders', [], function () { } // Set the new width to the slider. - sliderDiv.width(sliderWidth); - sliderDiv.css('display', 'inline-block'); + sliderEl.width(sliderWidth); + sliderEl.css('display', 'inline-block'); // Create a jQuery UI slider from the current DIV. We will set // starting parameters, and will also attach a handler to update // the state on the change event. - sliderDiv.slider({ + sliderEl.slider({ 'min': valueMin, 'max': valueMax, 'value': constValue, - 'step': 0.01, + 'step': (valueMax - valueMin) / 50.0, 'change': sliderOnChange }); + sliderEl.appendTo(spanEl); + return; function sliderOnChange(event, ui) { From 3682cb46c4522bee93c6acafa7e38c02cc052403 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Mon, 10 Dec 2012 18:37:20 +0200 Subject: [PATCH 036/125] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 15 +- .../js/src/graphical_slider_tool/inputs.js | 72 +++++++-- .../js/src/graphical_slider_tool/sliders.js | 137 ++++++++++++++---- 3 files changed, 186 insertions(+), 38 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 991cb0a26e..61228413f5 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -121,7 +121,20 @@ define('Graph', [], function () { } function updatePlot() { - $.plot(plotDiv, dataSets); + $.plot( + plotDiv, + dataSets, + { + 'xaxis': { + 'min': 0, + 'max': 30 + }, + 'yaxis': { + 'min': -5, + 'max': 5 + } + } + ); } } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js index d7f64328e0..3e7e55f02c 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/inputs.js @@ -2,12 +2,24 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Inputs', ['logme'], function (logme) { +define('Inputs', [], function () { return Inputs; function Inputs(gstId, config, state) { - logme('Inside "Inputs" module.'); - logme(gstId, config, state); + var constNamesUsed; + + // There should not be more than one text input per a constant. This + // just does not make sense. However, nothing really prevents the user + // from specifying more than one text input for the same constant name. + // That's why we have to track which constant names already have + // text inputs for them, and prevent adding further text inputs to + // these constants. + // + // constNamesUsed is an object to which we will add properties having + // the name of the constant to which we are adding a text input to. + // When creating a new text input, we must consult with this object, to + // see if the constant name is not defined as it's property. + constNamesUsed = {}; // We will go thorugh all of the inputs, and those that have a valid // '@var' property will be added to the page as a HTML text input @@ -15,42 +27,75 @@ define('Inputs', ['logme'], function (logme) { if ((typeof config.inputs !== 'undefined') && (typeof config.inputs.input !== 'undefined')) { if ($.isArray(config.inputs.input)) { - // config.inputs.input is an array + // config.inputs.input is an array. For each element, we will + // add a text input. for (c1 = 0; c1 < config.inputs.input.length; c1++) { createInput(config.inputs.input[c1]); } } else if ($.isPlainObject(config.inputs.input)) { - // config.inputs.input is an object + + // config.inputs.input is an object. Add a text input for it. createInput(config.inputs.input); + } } function createInput(obj) { var constName, constValue, spanEl, inputEl; + // The name of the constant is obj['@var']. If it is not specified, + // we will skip creating a text input for this constant. if (typeof obj['@var'] !== 'string') { return; } - constName = obj['@var']; - constValue = state.getConstValue(constName); - if (constValue === undefined) { - constValue = 0; + // We will not add a text input for a constant which already has a + // text input defined for it. + // + // We will add the constant name to the 'constNamesUsed' object in + // the end, when everything went successfully. + if (constNamesUsed.hasOwnProperty(constName)) { + return; } + // Multiple sliders and/or inputs can represent the same constant. + // Therefore we will get the most recent const value from the state + // object. If it is undefined, we will skip creating a text input + // for this constant. + constValue = state.getConstValue(constName); + if (constValue === undefined) { + return; + } + + // With the constant name, and the constant value being defined, + // lets get the element on the page into which the text input will + // be inserted. spanEl = $('#' + gstId + '_input_' + constName); + // If a corresponding element for this constant does not exist on + // the page, we will not be making a text input. if (spanEl.length === 0) { return; } + // Create the text input element. inputEl = $(''); - // inputEl.width(50); + // Set the current constant to the text input. It will be visible + // to the user. inputEl.val(constValue); + + // Bind a function to the 'change' event. Whenever the user changes + // the value of this text input, and presses 'enter' (or clicks + // somewhere else on the page), this event will be triggered, and + // our callback will be called. inputEl.bind('change', inputOnChange); + + // Lets style the input element nicely. We will use the button() + // widget for this since there is no native widget for the text + // input. inputEl.button().css({ 'font': 'inherit', 'color': 'inherit', @@ -61,10 +106,17 @@ define('Inputs', ['logme'], function (logme) { 'width': '50px' }); + // And finally, publish the text input element to the page. inputEl.appendTo(spanEl); + // Don't forget to add the constant to the list of used constants. + // Next time a slider for this constant will not be created. + constNamesUsed[constName] = true; + return; + // When the user changes the value of this text input, the 'state' + // will be updated, forcing the plot to be redrawn. function inputOnChange(event) { state.setConstValue(constName, $(this).val()); } diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js index 3db0c3e67c..33bdd89dd1 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/sliders.js @@ -6,68 +6,117 @@ define('Sliders', [], function () { return Sliders; function Sliders(gstId, config, state) { + var constNamesUsed; + + // There should not be more than one slider per a constant. This just + // does not make sense. However, nothing really prevents the user from + // specifying more than one slider for the same constant name. That's + // why we have to track which constant names already have sliders for + // them, and prevent adding further sliders to these constants. + // + // constNamesUsed is an object to which we will add properties having + // the name of the constant to which we are adding a slider to. When + // creating a new slider, we must consult with this object, to see if + // the constant name is not defined as it's property. + constNamesUsed = {}; + // We will go through all of the sliders. For each one, we will make a // jQuery UI slider for it, attach "on change" events, and set it's // state - initial value, max, and min parameters. if ((typeof config.sliders !== 'undefined') && (typeof config.sliders.slider !== 'undefined')) { if ($.isArray(config.sliders.slider)) { - // config.sliders.slider is an array + // config.sliders.slider is an array. For each object in the + // array, create a slider. for (c1 = 0; c1 < config.sliders.slider.length; c1++) { createSlider(config.sliders.slider[c1]); } + } else if ($.isPlainObject(config.sliders.slider)) { - // config.sliders.slider is an object + + // config.sliders.slider is an object. Create a slider for it. createSlider(config.sliders.slider); + } } function createSlider(obj) { - var constName, constValue, rangeBlobs, valueMin, valueMax, - spanEl, sliderEl, sliderWidth; - - // The name of the constant is obj['@var']. Multiple sliders and/or - // inputs can represent the same constant - therefore we will get - // the most recent const value from the state object. The range is - // a string composed of 3 blobs, separated by commas. The first - // blob is the min value for the slider, the third blob is the max - // value for the slider. + var constName, constValue, rangeBlobs, valueMin, valueMax, spanEl, + sliderEl, sliderWidth; + // The name of the constant is obj['@var']. If it is not specified, + // we will skip creating a slider for this constant. if (typeof obj['@var'] !== 'string') { return; } - constName = obj['@var']; - constValue = state.getConstValue(constName); - if (constValue === undefined) { - constValue = 0; + // We will not add a slider for a constant which already has a + // slider defined for it. + // + // We will add the constant name to the 'constNamesUsed' object in + // the end, when everything went successfully. + if (constNamesUsed.hasOwnProperty(constName)) { + return; } + // Multiple sliders and/or inputs can represent the same constant. + // Therefore we will get the most recent const value from the state + // object. If it is undefined, then something terrible has + // happened! We will skip creating a slider for this constant. + constValue = state.getConstValue(constName); + if (constValue === undefined) { + return; + } + + // The range is a string composed of 3 blobs, separated by commas. + // The first blob is the min value for the slider, the third blob + // is the max value for the slider. if (typeof obj['@range'] !== 'string') { + + // If the range is not a string, we will set a default range. + // No promise as to the quality of the data points that this + // range will produce. valueMin = constValue - 10; valueMax = constValue + 10; + } else { + + // Separate the range string by commas, and store each blob as + // an element in an array. rangeBlobs = obj['@range'].split(','); // We must have gotten exactly 3 blobs (pieces) from the split. if (rangeBlobs.length !== 3) { - valueMin = constValue - 10; - valueMax = constValue + 10; + + // Set some sensible defaults, if the range string was + // split into more or less than 3 pieces. + setDefaultMinMax(); + } else { - // Get the first blob from the split string. + + // Get the first blob from the split string. It is the min + // value. valueMin = parseFloat(rangeBlobs[0]); + // Is it a well-formed float number? if (isNaN(valueMin) === true) { + + // No? Then set a sensible default value. valueMin = constValue - 10; + } - // Get the third blob from the split string. + // Get the third blob from the split string. It is the max. valueMax = parseFloat(rangeBlobs[2]); + // Is it a well-formed float number? if (isNaN(valueMax) === true) { + + // No? Then set a sensible default value. valueMax = constValue + 10; + } // Logically, the min, value, and max should make sense. @@ -79,38 +128,53 @@ define('Sliders', [], function () { if ((valueMin > valueMax) || (valueMin > constValue) || (valueMax < constValue)) { - valueMin = constValue - 10; - valueMax = constValue + 10; + + // Set some sensible defaults, if min/value/max logic + // is broken. + setDefaultMinMax(); + } } } + // At this point we have the constant name, the constant value, and + // the min and max values for this slider. Lets get the element on + // the page into which the slider will be inserted. spanEl = $('#' + gstId + '_slider_' + constName); - // If a corresponding slider DIV for this constant does not exist, - // do not do anything. + // If a corresponding element for this constant does not exist on + // the page, we will not be making a slider. if (spanEl.length === 0) { return; } + // Create the slider DIV. sliderEl = $('
'); - // The default slider width. + // We will define the width of the slider to a sensible default. sliderWidth = 400; + // Then we will see if one is provided in the config for this + // slider. If we find it, and it is a well-formed integer, we will + // use it, instead of the default width. if (typeof obj['@width'] === 'string') { if (isNaN(parseInt(obj['@width'], 10)) === false) { sliderWidth = parseInt(obj['@width'], 10); } } - // Set the new width to the slider. + // Set the defined width to the slider. sliderEl.width(sliderWidth); + + // And make sure that it gets added to the page as an + // 'inline-block' element. This will allow for the insertion of the + // slider into a paragraph, without the browser forcing it out of + // the paragraph onto a new line, separate line. sliderEl.css('display', 'inline-block'); - // Create a jQuery UI slider from the current DIV. We will set + // Create a jQuery UI slider from the slider DIV. We will set // starting parameters, and will also attach a handler to update - // the state on the change event. + // the 'state' on the 'change' event. sliderEl.slider({ 'min': valueMin, 'max': valueMax, @@ -120,13 +184,32 @@ define('Sliders', [], function () { 'change': sliderOnChange }); + // Append the slider DIV to the element on the page where the user + // wants to see it. sliderEl.appendTo(spanEl); + // OK! So we made it this far... + // + // Adding the constant to the list of used constants. Next time a + // slider for this constant will not be created. + constNamesUsed[constName] = true; + return; + // Update the 'state' - i.e. set the value of the constant this + // slider is attached to to a new value. + // + // This will cause the plot to be redrawn each time after the user + // drags the slider handle and releases it. function sliderOnChange(event, ui) { state.setConstValue(constName, ui.value); } + + // The sensible defaults for the slider's range. + function setDefaultMinMax() { + valueMin = constValue - 10; + valueMax = constValue + 10; + } } } }); From 28f4921924c1e47c7a68a754292a63dcdea6c35e Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Tue, 11 Dec 2012 06:56:43 +0200 Subject: [PATCH 037/125] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index 61228413f5..dba0483674 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -2,13 +2,15 @@ // define() functions from Require JS available inside the anonymous function. (function (requirejs, require, define) { -define('Graph', [], function () { +define('Graph', ['logme'], function (logme) { return Graph; function Graph(gstId, config, state) { var plotDiv, dataSets, functions; + logme(config); + plotDiv = $('#' + gstId + '_plot'); if (plotDiv.length === 0) { @@ -39,11 +41,29 @@ define('Graph', [], function () { if (typeof config.plot['function'] === 'string') { addFunction(config.plot['function']); } else if ($.isPlainObject(config.plot['function']) === true) { - + addFunction( + config.plot['function']['#text'], + config.plot['function']['@color'], + config.plot['function']['@dot'], + config.plot['function']['@label'], + config.plot['function']['@line'], + config.plot['function']['@point_size'], + config.plot['function']['@style'] + ); } else if ($.isArray(config.plot['function'])) { for (c1 = 0; c1 < config.plot['function'].length; c1++) { if (typeof config.plot['function'][c1] === 'string') { addFunction(config.plot['function'][c1]); + } else if ($.isPlainObject(config.plot['function'][c1])) { + addFunction( + config.plot['function'][c1]['#text'], + config.plot['function'][c1]['@color'], + config.plot['function'][c1]['@dot'], + config.plot['function'][c1]['@label'], + config.plot['function'][c1]['@line'], + config.plot['function'][c1]['@point_size'], + config.plot['function'][c1]['@style'] + ); } } } @@ -76,17 +96,31 @@ define('Graph', [], function () { } if (typeof line === 'boolean') { - newFunctionObject['line'] = line; + if ((line === 'true') || (line === true)) { + newFunctionObject['line'] = true; + } else { + newFunctionObject['line'] = false; + } } - if (typeof dot === 'boolean') { - newFunctionObject['dot'] = dot; + if ((typeof dot === 'boolean') || (typeof dot === 'string')) { + if ((dot === 'true') || (dot === true)) { + newFunctionObject['dot'] = true; + } else { + newFunctionObject['dot'] = false; + } + } + + if ((newFunctionObject['dot'] === false) && (newFunctionObject['line'] === false)) { + newFunctionObject['line'] = true; } if (typeof label === 'string') { newFunctionObject['label'] = label; } + logme(newFunctionObject); + functions.push(newFunctionObject); } } From 3e46ecef646f194798dff81dc4f4176a9e69a27f Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Tue, 11 Dec 2012 10:47:46 +0200 Subject: [PATCH 038/125] GST work in progress. --- .../js/src/graphical_slider_tool/graph.js | 132 ++++++++++++++---- .../js/src/graphical_slider_tool/gst_main.js | 4 + .../js/graphical_slider_tool/gst_module.js | 15 -- .../static/js/graphical_slider_tool/main.js | 75 ---------- 4 files changed, 108 insertions(+), 118 deletions(-) delete mode 100644 common/static/js/graphical_slider_tool/gst_module.js delete mode 100644 common/static/js/graphical_slider_tool/main.js diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js index dba0483674..c0c8addf80 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/graph.js @@ -7,7 +7,7 @@ define('Graph', ['logme'], function (logme) { return Graph; function Graph(gstId, config, state) { - var plotDiv, dataSets, functions; + var plotDiv, dataSeries, functions; logme(config); @@ -39,37 +39,55 @@ define('Graph', ['logme'], function (logme) { } if (typeof config.plot['function'] === 'string') { + + // If just one function string is present. addFunction(config.plot['function']); + } else if ($.isPlainObject(config.plot['function']) === true) { - addFunction( - config.plot['function']['#text'], - config.plot['function']['@color'], - config.plot['function']['@dot'], - config.plot['function']['@label'], - config.plot['function']['@line'], - config.plot['function']['@point_size'], - config.plot['function']['@style'] - ); + + // If a function is present, but it also has properties + // defined. + callAddFunction(config.plot['function']); + } else if ($.isArray(config.plot['function'])) { + + // If more than one function is defined. for (c1 = 0; c1 < config.plot['function'].length; c1++) { + + // For each definition, we must check if it is a simple + // string definition, or a complex one with properties. if (typeof config.plot['function'][c1] === 'string') { + + // Simple string. addFunction(config.plot['function'][c1]); + } else if ($.isPlainObject(config.plot['function'][c1])) { - addFunction( - config.plot['function'][c1]['#text'], - config.plot['function'][c1]['@color'], - config.plot['function'][c1]['@dot'], - config.plot['function'][c1]['@label'], - config.plot['function'][c1]['@line'], - config.plot['function'][c1]['@point_size'], - config.plot['function'][c1]['@style'] - ); + + // Properties are present. + callAddFunction(config.plot['function'][c1]); + } } } return; + // This function will reduce code duplications. We have to call + // the function addFunction() several times passing object + // properties. A parameters. Rather than writing them out every + // time, we will have a single point of + function callAddFunction(obj) { + addFunction( + obj['#text'], + obj['@color'], + obj['@line'], + obj['@dot'], + obj['@label'], + obj['@style'], + obj['@point_size'] + ); + } + function addFunction(funcString, color, line, dot, label, style, point_size) { var newFunctionObject, func, constNames; @@ -95,7 +113,7 @@ define('Graph', ['logme'], function (logme) { newFunctionObject['color'] = color; } - if (typeof line === 'boolean') { + if ((typeof line === 'boolean') || (typeof line === 'string')) { if ((line === 'true') || (line === true)) { newFunctionObject['line'] = true; } else { @@ -111,6 +129,9 @@ define('Graph', ['logme'], function (logme) { } } + // By default, if no preference was set, or if the preference + // is conflicting (we must have either line or dot, none is + // not an option), we will show line. if ((newFunctionObject['dot'] === false) && (newFunctionObject['line'] === false)) { newFunctionObject['line'] = true; } @@ -131,33 +152,68 @@ define('Graph', ['logme'], function (logme) { } function generateData() { - var c0, c1, datapoints, constValues, x, y; + var c0, c1, functionObj, seriesObj, dataPoints, constValues, x, y; constValues = state.getAllConstantValues(); - dataSets = []; + dataSeries = []; for (c0 = 0; c0 < functions.length; c0 += 1) { - datapoints = []; + functionObj = functions[c0]; + logme('Functions obj:', functionObj); - for (c1 = 0; c1 < 30; c1 += 0.1) { + seriesObj = {}; + dataPoints = []; + + for (c1 = 0; c1 < 30; c1 += 1) { x = c1; + // Push the 'x' variable to the end of the parameter array. constValues.push(x); - y = functions[c0].func.apply(window, constValues); + + // We call the user defined function, passing all of the + // available constant values. inside this function they + // will be accessible by their names. + y = functionObj.func.apply(window, constValues); + + // Return the constValues array to how it was before we + // added 'x' variable to the end of it. constValues.pop(); - datapoints.push([x, y]); + // Add the generated point to the data points set. + dataPoints.push([x, y]); + } - dataSets.push(datapoints); + // Put the entire data points set into the series object. + seriesObj.data = dataPoints; + + // See if user defined a specific color for this function. + if (functionObj.hasOwnProperty('color') === true) { + seriesObj.color = functionObj.color; + } + + // See if a user defined a label for this function. + if (functionObj.hasOwnProperty('label') === true) { + seriesObj.label = functionObj.label; + } + + seriesObj.lines = { + 'show': functionObj.line + }; + + seriesObj.points = { + 'show': functionObj.dot + }; + + dataSeries.push(seriesObj); } } function updatePlot() { $.plot( plotDiv, - dataSets, + dataSeries, { 'xaxis': { 'min': 0, @@ -166,9 +222,29 @@ define('Graph', ['logme'], function (logme) { 'yaxis': { 'min': -5, 'max': 5 + }, + 'legend': { + + // To show the legend or not. Note, even if 'show' is + // 'true', the legend will only show if labels are + // provided for at least one of the series that are + // going to be plotted. + 'show': true, + + // A floating point number in the range [0, 1]. The + // smaller the number, the more transparent will the + // legend background become. + 'backgroundOpacity': 0 + } } ); + + MathJax.Hub.Queue([ + 'Typeset', + MathJax.Hub, + plotDiv.attr('id') + ]); } } }); diff --git a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js index 47881b66c6..8611fed1f2 100644 --- a/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js +++ b/common/lib/xmodule/xmodule/js/src/graphical_slider_tool/gst_main.js @@ -4,6 +4,10 @@ define( 'GstMain', + + // Even though it is not explicitly in this module, we have to specify + // 'GeneralMethods' as a dependency. It expands some of the core JS objects + // with additional useful methods that are used in other modules. ['State', 'GeneralMethods', 'Sliders', 'Inputs', 'Graph'], function (State, GeneralMethods, Sliders, Inputs, Graph) { diff --git a/common/static/js/graphical_slider_tool/gst_module.js b/common/static/js/graphical_slider_tool/gst_module.js deleted file mode 100644 index c4661b5e44..0000000000 --- a/common/static/js/graphical_slider_tool/gst_module.js +++ /dev/null @@ -1,15 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -define([], function () { - return { - 'module_status': 'OK' - }; -}); - -// End of wrapper for RequireJS. As you can see, we are passing -// namespaced Require JS variables to an anonymous function. Within -// it, you can use the standard requirejs(), require(), and define() -// functions as if they were in the global namespace. -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); // End-of: (function (requirejs, require, define) diff --git a/common/static/js/graphical_slider_tool/main.js b/common/static/js/graphical_slider_tool/main.js deleted file mode 100644 index da36d9c9d6..0000000000 --- a/common/static/js/graphical_slider_tool/main.js +++ /dev/null @@ -1,75 +0,0 @@ -// Wrapper for RequireJS. It will make the standard requirejs(), require(), and -// define() functions from Require JS available inside the anonymous function. -(function (requirejs, require, define) { - -// For documentation please check: -// http://requirejs.org/docs/api.html -requirejs.config({ - // Because require.js is included as a simple