From dba39e243174f28a1245103721194ef33a7300ca Mon Sep 17 00:00:00 2001
From: Your Name
Date: Fri, 18 Jan 2013 12:50:25 -0500
Subject: [PATCH 01/82] add group support to lms
---
lms/djangoapps/courseware/courses.py | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 7c0d30ebd8..90ebbd33da 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -58,6 +58,29 @@ def get_opt_course_with_access(user, course_id, action):
return get_course_with_access(user, course_id, action)
+
+
+def is_course_cohorted(course_id):
+ """
+ given a course id, return a boolean for whether or not the course is cohorted
+
+ """
+
+def get_cohort_id(user, course_id):
+ """
+ given a course id and a user, return the id of the cohort that user is assigned to
+ and if the course is not cohorted or the user is an instructor, return None
+
+ """
+
+def is_commentable_cohorted(course_id,commentable_id)
+ """
+ given a course and a commentable id, return whether or not this commentable is cohorted
+
+ """
+
+
+
def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
From b8e3cfcf96a143294df5482f904d8e45c10becf9 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Fri, 18 Jan 2013 13:50:05 -0500
Subject: [PATCH 02/82] added get_cohort_ids
---
lms/djangoapps/courseware/courses.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 90ebbd33da..8fd4497baf 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -78,8 +78,12 @@ def is_commentable_cohorted(course_id,commentable_id)
given a course and a commentable id, return whether or not this commentable is cohorted
"""
+
-
+def get_cohort_ids(course_id):
+ """
+ given a course id, return an array of all cohort ids for that course (needed for UI
+ """
def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
From b26c59bda0292c801400d104abb0ba5097ce18cf Mon Sep 17 00:00:00 2001
From: Kevin Chugh
Date: Sun, 20 Jan 2013 01:53:25 -0500
Subject: [PATCH 03/82] fix syntax error
---
lms/djangoapps/courseware/courses.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 8fd4497baf..551e459492 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -73,7 +73,7 @@ def get_cohort_id(user, course_id):
"""
-def is_commentable_cohorted(course_id,commentable_id)
+def is_commentable_cohorted(course_id,commentable_id):
"""
given a course and a commentable id, return whether or not this commentable is cohorted
From 59b3dc61a286dbac4a66d90cea1a98b43dab07a6 Mon Sep 17 00:00:00 2001
From: Kevin Chugh
Date: Wed, 23 Jan 2013 16:25:26 -0500
Subject: [PATCH 04/82] lms producing grouped content
---
lms/djangoapps/courseware/courses.py | 1 +
.../django_comment_client/base/views.py | 19 +++++++++++++++++++
.../django_comment_client/forum/views.py | 7 +++++++
lms/lib/comment_client/thread.py | 4 ++--
4 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 551e459492..057ef42a58 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -72,6 +72,7 @@ def get_cohort_id(user, course_id):
and if the course is not cohorted or the user is an instructor, return None
"""
+ return 101
def is_commentable_cohorted(course_id,commentable_id):
"""
diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py
index 63d69427c9..d1948c3fc7 100644
--- a/lms/djangoapps/django_comment_client/base/views.py
+++ b/lms/djangoapps/django_comment_client/base/views.py
@@ -21,6 +21,7 @@ from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
+from courseware.courses import get_cohort_id
from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context
@@ -83,6 +84,24 @@ def create_thread(request, course_id, commentable_id):
'course_id' : course_id,
'user_id' : request.user.id,
})
+
+ #now cohort id
+ #if the group id came in from the form, set it there, otherwise,
+ #see if the user and the commentable are cohorted
+ print post
+
+ group_id = None
+
+ if 'group_id' in post:
+ group_id = post['group_id']
+
+
+ if group_id is None:
+ group_id = get_cohort_id(request.user, course_id)
+
+ if group_id is not None:
+ thread.update_attributes(**{'group_id' :group_id})
+
thread.save()
if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user)
diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py
index 35e7fd6618..0e8a044097 100644
--- a/lms/djangoapps/django_comment_client/forum/views.py
+++ b/lms/djangoapps/django_comment_client/forum/views.py
@@ -11,6 +11,7 @@ from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
+from courseware.courses import get_cohort_id
from courseware.access import has_access
from urllib import urlencode
@@ -58,6 +59,12 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
user.default_sort_key = request.GET.get('sort_key')
user.save()
+
+ #if the course-user is cohorted, then add the group id
+ group_id = get_cohort_id(user,course_id);
+ if group_id:
+ default_query_params["group_id"] = group_id;
+
query_params = merge_dict(default_query_params,
strip_none(extract(request.GET, ['page', 'sort_key', 'sort_order', 'text', 'tags', 'commentable_ids'])))
diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py
index 424250033e..6fd31b0823 100644
--- a/lms/lib/comment_client/thread.py
+++ b/lms/lib/comment_client/thread.py
@@ -10,12 +10,12 @@ class Thread(models.Model):
'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id',
'created_at', 'updated_at', 'comments_count', 'unread_comments_count',
'at_position_list', 'children', 'type', 'highlighted_title',
- 'highlighted_body', 'endorsed', 'read'
+ 'highlighted_body', 'endorsed', 'read', 'group_id'
]
updatable_fields = [
'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
- 'closed', 'tags', 'user_id', 'commentable_id',
+ 'closed', 'tags', 'user_id', 'commentable_id', 'group_id'
]
initializable_fields = updatable_fields
From 1d13b1a9bd6ec595b35c727468c484b2a36f3047 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Wed, 16 Jan 2013 09:45:22 -0500
Subject: [PATCH 05/82] Make various changes to handle the s3/sftp part of the
pearson process.
---
.../student/management/commands/pearson.py | 74 +++++++++++++++++++
.../management/commands/pearson_export_cdd.py | 32 +++-----
.../management/commands/pearson_export_ead.py | 26 +++----
3 files changed, 93 insertions(+), 39 deletions(-)
create mode 100644 common/djangoapps/student/management/commands/pearson.py
diff --git a/common/djangoapps/student/management/commands/pearson.py b/common/djangoapps/student/management/commands/pearson.py
new file mode 100644
index 0000000000..2a8b02928c
--- /dev/null
+++ b/common/djangoapps/student/management/commands/pearson.py
@@ -0,0 +1,74 @@
+from optparse import make_option
+
+from django.contrib.auth.models import User
+from django.core.management.base import BaseCommand, CommandError
+import re
+from dogapi import dog_http_api, dog_stats_api
+import paramiko
+
+dog_http_api.api_key = settings.DATADOG_API
+
+
+class Command(BaseCommand):
+
+ option_list = BaseCommand.option_list
+ args = ''
+ help = """
+ Mode should be import or export depending on if you're fetching from pearson or
+ sending to them.
+ """
+
+ def handle(self, *args):
+ if len(args) < 1:
+ raise CommandError('Usage is pearson {0}'.format(self.args))
+
+ for mode in args:
+ if mode == 'export':
+ sftp(settings.PEARSON_LOCAL_IMPORT, settings.PEARSON_SFTP_IMPORT)
+ s3(settings.PEARSON_LOCAL, settings.PEARSON_BUCKET)
+ elif mode == 'import':
+ sftp(settings.PEARSON_SFTP_EXPORT, settings.PEARSON_LOCAL_EXPORT)
+ s3(settings.PEARSON_LOCAL_EXPORT, settings.PEARSON_BUCKET)
+ else:
+ print("ERROR: Mode must be export or import.")
+
+ def sftp(files_from, files_to):
+ with dog_stats_api.timer('pearson.{0}'.format(mode), tags='sftp'):
+ try:
+ t = paramiko.Transport((hostname, 22))
+ t.connect(username=settings.PEARSON_SFTP_USERNAME,
+ password=settings.PEARSON_SFTP_PASSWORD)
+ sftp = paramiko.SFTPClient.from_transport(t)
+ if os.path.isdir(files_from):
+ for file in os.listdir(files_from):
+ sftp.put(files_from+'/'+filename,
+ files_to+'/'+filename)
+ else:
+ for file in sftp.listdir(files_from):
+ sftp.get(files_from+'/'+filename,
+ files_to+'/'+filename)
+ except:
+ dog_http_api.event('pearson {0}'.format(mode),
+ 'sftp uploading failed', alert_type='error')
+ raise
+
+ def s3(files_from, bucket):
+ with dog_stats_api.timer('pearson.{0}'.format(mode), tags='s3'):
+ try:
+ for filename in os.listdir(files):
+ upload_file_to_s3(bucket, files_from+'/'+filename)
+ except:
+ dog_http_api.event('pearson {0}'.format(mode), 's3 archiving failed')
+ raise
+
+
+ def upload_file_to_s3(bucket, filename):
+ """
+ Upload file to S3
+ """
+ s3 = boto.connect_s3()
+ from boto.s3.key import Key
+ b = s3.get_bucket(bucket)
+ k = Key(b)
+ k.key = "{filename}".format(filename=filename)
+ k.set_contents_from_filename(filename)
diff --git a/common/djangoapps/student/management/commands/pearson_export_cdd.py b/common/djangoapps/student/management/commands/pearson_export_cdd.py
index 67230c7f74..cebc098080 100644
--- a/common/djangoapps/student/management/commands/pearson_export_cdd.py
+++ b/common/djangoapps/student/management/commands/pearson_export_cdd.py
@@ -37,39 +37,25 @@ class Command(BaseCommand):
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
- option_list = BaseCommand.option_list + (
- make_option(
- '--dump_all',
- action='store_true',
- dest='dump_all',
- ),
- )
-
- args = ''
- help = """
- Export user demographic information from TestCenterUser model into a tab delimited
- text file with a format that Pearson expects.
- """
- def handle(self, *args, **kwargs):
- if len(args) < 1:
- print Command.help
- return
-
+ def handle(self, **kwargs):
# update time should use UTC in order to be comparable to the user_updated_at
# field
uploaded_at = datetime.utcnow()
# if specified destination is an existing directory, then
# create a filename for it automatically. If it doesn't exist,
- # or exists as a file, then we will just write to it.
+ # then we will create the directory.
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
- dest = args[0]
- if isdir(dest):
- destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
+ if not os.path.isdir(settings.PEARSON_LOCAL_EXPORT):
+ os.makedirs(settings.PEARSON_LOCAL_EXPORT)
+ destfile = os.path.join(settings.PEARSON_LOCAL_EXPORT,
+ uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
else:
- destfile = dest
+ destfile = os.path.join(settings.PEARSON_LOCAL_EXPORT,
+ uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
+
# strings must be in latin-1 format. CSV parser will
# otherwise convert unicode objects to ascii.
diff --git a/common/djangoapps/student/management/commands/pearson_export_ead.py b/common/djangoapps/student/management/commands/pearson_export_ead.py
index de3bfc04ee..5423282346 100644
--- a/common/djangoapps/student/management/commands/pearson_export_ead.py
+++ b/common/djangoapps/student/management/commands/pearson_export_ead.py
@@ -23,11 +23,6 @@ class Command(BaseCommand):
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
- args = ''
- help = """
- Export user registration information from TestCenterRegistration model into a tab delimited
- text file with a format that Pearson expects.
- """
option_list = BaseCommand.option_list + (
make_option(
@@ -43,26 +38,25 @@ class Command(BaseCommand):
)
- def handle(self, *args, **kwargs):
- if len(args) < 1:
- print Command.help
- return
+ def handle(self, **kwargs):
# update time should use UTC in order to be comparable to the user_updated_at
# field
uploaded_at = datetime.utcnow()
- # if specified destination is an existing directory, then
+ # if specified destination is an existing directory, then
# create a filename for it automatically. If it doesn't exist,
- # or exists as a file, then we will just write to it.
+ # then we will create the directory.
# Name will use timestamp -- this is UTC, so it will look funny,
- # but it should at least be consistent with the other timestamps
+ # but it should at least be consistent with the other timestamps
# used in the system.
- dest = args[0]
- if isdir(dest):
- destfile = join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
+ if not os.path.isdir(settings.PEARSON_LOCAL_EXPORT):
+ os.makedirs(settings.PEARSON_LOCAL_EXPORT)
+ destfile = os.path.join(settings.PEARSON_LOCAL_EXPORT,
+ uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
else:
- destfile = dest
+ destfile = os.path.join(settings.PEARSON_LOCAL_EXPORT,
+ uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
dump_all = kwargs['dump_all']
From 1acf2dbba350272b06df29fe25d71f248060f1c4 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Wed, 16 Jan 2013 09:52:04 -0500
Subject: [PATCH 06/82] Use a dictionary for all the pearson stuff to keep the
auth/env stuff clean.
---
.../student/management/commands/pearson.py | 12 +++++++-----
.../management/commands/pearson_export_cdd.py | 8 ++++----
.../management/commands/pearson_export_ead.py | 8 ++++----
3 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson.py b/common/djangoapps/student/management/commands/pearson.py
index 2a8b02928c..a317b08d62 100644
--- a/common/djangoapps/student/management/commands/pearson.py
+++ b/common/djangoapps/student/management/commands/pearson.py
@@ -5,6 +5,7 @@ from django.core.management.base import BaseCommand, CommandError
import re
from dogapi import dog_http_api, dog_stats_api
import paramiko
+import boto
dog_http_api.api_key = settings.DATADOG_API
@@ -24,11 +25,11 @@ class Command(BaseCommand):
for mode in args:
if mode == 'export':
- sftp(settings.PEARSON_LOCAL_IMPORT, settings.PEARSON_SFTP_IMPORT)
- s3(settings.PEARSON_LOCAL, settings.PEARSON_BUCKET)
+ sftp(settings.PEARSON[LOCAL_IMPORT], settings.PEARSON[SFTP_IMPORT])
+ s3(settings.PEARSON_LOCAL, settings.PEARSON[BUCKET])
elif mode == 'import':
- sftp(settings.PEARSON_SFTP_EXPORT, settings.PEARSON_LOCAL_EXPORT)
- s3(settings.PEARSON_LOCAL_EXPORT, settings.PEARSON_BUCKET)
+ sftp(settings.PEARSON[SFTP_EXPORT], settings.PEARSON[LOCAL_EXPORT])
+ s3(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[BUCKET])
else:
print("ERROR: Mode must be export or import.")
@@ -66,7 +67,8 @@ class Command(BaseCommand):
"""
Upload file to S3
"""
- s3 = boto.connect_s3()
+ s3 = boto.connect_s3(settings.AWS_ACCESS_KEY_ID,
+ settings.AWS_SECRET_ACCESS_KEY)
from boto.s3.key import Key
b = s3.get_bucket(bucket)
k = Key(b)
diff --git a/common/djangoapps/student/management/commands/pearson_export_cdd.py b/common/djangoapps/student/management/commands/pearson_export_cdd.py
index cebc098080..dcb9f5cd97 100644
--- a/common/djangoapps/student/management/commands/pearson_export_cdd.py
+++ b/common/djangoapps/student/management/commands/pearson_export_cdd.py
@@ -48,12 +48,12 @@ class Command(BaseCommand):
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
- if not os.path.isdir(settings.PEARSON_LOCAL_EXPORT):
- os.makedirs(settings.PEARSON_LOCAL_EXPORT)
- destfile = os.path.join(settings.PEARSON_LOCAL_EXPORT,
+ if not os.path.isdir(settings.PEARSON[LOCAL_EXPORT]):
+ os.makedirs(settings.PEARSON[LOCAL_EXPORT])
+ destfile = os.path.join(settings.PEARSON[LOCAL_EXPORT],
uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
else:
- destfile = os.path.join(settings.PEARSON_LOCAL_EXPORT,
+ destfile = os.path.join(settings.PEARSON[LOCAL_EXPORT],
uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
diff --git a/common/djangoapps/student/management/commands/pearson_export_ead.py b/common/djangoapps/student/management/commands/pearson_export_ead.py
index 5423282346..9520e9d013 100644
--- a/common/djangoapps/student/management/commands/pearson_export_ead.py
+++ b/common/djangoapps/student/management/commands/pearson_export_ead.py
@@ -50,12 +50,12 @@ class Command(BaseCommand):
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
- if not os.path.isdir(settings.PEARSON_LOCAL_EXPORT):
- os.makedirs(settings.PEARSON_LOCAL_EXPORT)
- destfile = os.path.join(settings.PEARSON_LOCAL_EXPORT,
+ if not os.path.isdir(settings.PEARSON[LOCAL_EXPORT]):
+ os.makedirs(settings.PEARSON[LOCAL_EXPORT])
+ destfile = os.path.join(settings.PEARSON[LOCAL_EXPORT],
uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
else:
- destfile = os.path.join(settings.PEARSON_LOCAL_EXPORT,
+ destfile = os.path.join(settings.PEARSON[LOCAL_EXPORT],
uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
dump_all = kwargs['dump_all']
From 61ea2d7b5da004053073c575c6bb4a8a066296dc Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Wed, 16 Jan 2013 10:02:10 -0500
Subject: [PATCH 07/82] Couple of fixes to the settings data.
---
.../student/management/commands/pearson.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson.py b/common/djangoapps/student/management/commands/pearson.py
index a317b08d62..0752aea8be 100644
--- a/common/djangoapps/student/management/commands/pearson.py
+++ b/common/djangoapps/student/management/commands/pearson.py
@@ -25,11 +25,11 @@ class Command(BaseCommand):
for mode in args:
if mode == 'export':
- sftp(settings.PEARSON[LOCAL_IMPORT], settings.PEARSON[SFTP_IMPORT])
- s3(settings.PEARSON_LOCAL, settings.PEARSON[BUCKET])
- elif mode == 'import':
- sftp(settings.PEARSON[SFTP_EXPORT], settings.PEARSON[LOCAL_EXPORT])
+ sftp(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[SFTP_EXPORT])
s3(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[BUCKET])
+ elif mode == 'import':
+ sftp(settings.PEARSON[SFTP_IMPORT], settings.PEARSON[LOCAL_IMPORT])
+ s3(settings.PEARSON[LOCAL_IMPORT], settings.PEARSON[BUCKET])
else:
print("ERROR: Mode must be export or import.")
@@ -37,8 +37,8 @@ class Command(BaseCommand):
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='sftp'):
try:
t = paramiko.Transport((hostname, 22))
- t.connect(username=settings.PEARSON_SFTP_USERNAME,
- password=settings.PEARSON_SFTP_PASSWORD)
+ t.connect(username=settings.PEARSON[SFTP_USERNAME],
+ password=settings.PEARSON[SFTP_PASSWORD])
sftp = paramiko.SFTPClient.from_transport(t)
if os.path.isdir(files_from):
for file in os.listdir(files_from):
From ced29a25ece11ea50799515f22f307a1e19d74e7 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Wed, 16 Jan 2013 10:17:30 -0500
Subject: [PATCH 08/82] Add paramiko to requirements.
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 996388a51d..fa4688b711 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -58,4 +58,4 @@ factory_boy
Shapely==1.2.16
ipython==0.13.1
xmltodict==0.4.1
-
+paramiko==1.9.0
From 378b1ff0c35c809558bb081f5acc09be9a516db4 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Wed, 16 Jan 2013 11:26:39 -0500
Subject: [PATCH 09/82] Various changes thanks to feedback from Brian to make
the existing export commands handle --dest-from-settings and --destination
and fail unless one is provided as well as rename pearson.py to
pearson_transfer and allow is to call the import/export commands directly.
I've set it to die in pearson_transfer.py if the django PEARSON settings
aren't available. I don't want to try and provide defaults, these must
exist or it simply fails.
---
.../management/commands/pearson_export_cdd.py | 37 ++++++++++---
.../management/commands/pearson_export_ead.py | 52 ++++++++++++-------
.../{pearson.py => pearson_transfer.py} | 50 +++++++++++-------
3 files changed, 95 insertions(+), 44 deletions(-)
rename common/djangoapps/student/management/commands/{pearson.py => pearson_transfer.py} (63%)
diff --git a/common/djangoapps/student/management/commands/pearson_export_cdd.py b/common/djangoapps/student/management/commands/pearson_export_cdd.py
index dcb9f5cd97..c2c13916ab 100644
--- a/common/djangoapps/student/management/commands/pearson_export_cdd.py
+++ b/common/djangoapps/student/management/commands/pearson_export_cdd.py
@@ -37,7 +37,20 @@ class Command(BaseCommand):
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
- def handle(self, **kwargs):
+ option_list = BaseCommand.option_list + (
+ make_option('--dest-from-settings',
+ action='store_true',
+ dest='dest-from-settings',
+ default=False,
+ help='Retrieve the destination to export to from django? True/False'),
+ make_option('--destination',
+ action='store_true',
+ dest='destination',
+ default=None,
+ help='Where to store the exported files')
+ )
+
+ def handle(self, **options):
# update time should use UTC in order to be comparable to the user_updated_at
# field
uploaded_at = datetime.utcnow()
@@ -48,13 +61,23 @@ class Command(BaseCommand):
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
- if not os.path.isdir(settings.PEARSON[LOCAL_EXPORT]):
- os.makedirs(settings.PEARSON[LOCAL_EXPORT])
- destfile = os.path.join(settings.PEARSON[LOCAL_EXPORT],
- uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
+ if options['dest-from-settings'] is True:
+ if settings.PEARSON[LOCAL_EXPORT]:
+ dest = settings.PEARSON[LOCAL_EXPORT]
+ else:
+ raise CommandError('--dest-from-settings was enabled but the'
+ 'PEARSON[LOCAL_EXPORT] setting was not set.')
+ elif options['destination']:
+ dest = options['destination']
else:
- destfile = os.path.join(settings.PEARSON[LOCAL_EXPORT],
- uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
+ raise ComamndError('--destination or --dest-from-settings must be used')
+
+
+ if not os.path.isdir(dest):
+ os.makedirs(dest)
+ destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
+ else:
+ destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
# strings must be in latin-1 format. CSV parser will
diff --git a/common/djangoapps/student/management/commands/pearson_export_ead.py b/common/djangoapps/student/management/commands/pearson_export_ead.py
index 9520e9d013..6e3149778d 100644
--- a/common/djangoapps/student/management/commands/pearson_export_ead.py
+++ b/common/djangoapps/student/management/commands/pearson_export_ead.py
@@ -23,24 +23,29 @@ class Command(BaseCommand):
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
-
option_list = BaseCommand.option_list + (
- make_option(
- '--dump_all',
- action='store_true',
- dest='dump_all',
+ make_option('--dest-from-settings',
+ action='store_true',
+ dest='dest-from-settings',
+ default=False,
+ help='Retrieve the destination to export to from django? True/False'),
+ make_option('--destination',
+ action='store_true',
+ dest='destination',
+ default=None,
+ help='Where to store the exported files'),
+ make_option('--dump_all',
+ action='store_true',
+ dest='dump_all',
),
- make_option(
- '--force_add',
- action='store_true',
- dest='force_add',
+ make_option('--force_add',
+ action='store_true',
+ dest='force_add',
),
)
-
-
- def handle(self, **kwargs):
- # update time should use UTC in order to be comparable to the user_updated_at
+ def handle(self, **options):
+ # update time should use UTC in order to be comparable to the user_updated_at
# field
uploaded_at = datetime.utcnow()
@@ -50,13 +55,22 @@ class Command(BaseCommand):
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
- if not os.path.isdir(settings.PEARSON[LOCAL_EXPORT]):
- os.makedirs(settings.PEARSON[LOCAL_EXPORT])
- destfile = os.path.join(settings.PEARSON[LOCAL_EXPORT],
- uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
+ if options['dest-from-settings'] is True:
+ if settings.PEARSON[LOCAL_EXPORT]:
+ dest = settings.PEARSON[LOCAL_EXPORT]
+ else:
+ raise CommandError('--dest-from-settings was enabled but the'
+ 'PEARSON[LOCAL_EXPORT] setting was not set.')
+ elif options['destination']:
+ dest = options['destination']
else:
- destfile = os.path.join(settings.PEARSON[LOCAL_EXPORT],
- uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
+ raise ComamndError('--destination or --dest-from-settings must be used')
+
+ if not os.path.isdir(dest):
+ os.makedirs(dest)
+ destfile = os.path.join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
+ else:
+ destfile = os.path.join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
dump_all = kwargs['dump_all']
diff --git a/common/djangoapps/student/management/commands/pearson.py b/common/djangoapps/student/management/commands/pearson_transfer.py
similarity index 63%
rename from common/djangoapps/student/management/commands/pearson.py
rename to common/djangoapps/student/management/commands/pearson_transfer.py
index 0752aea8be..1d04936216 100644
--- a/common/djangoapps/student/management/commands/pearson.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -12,26 +12,40 @@ dog_http_api.api_key = settings.DATADOG_API
class Command(BaseCommand):
- option_list = BaseCommand.option_list
- args = ''
- help = """
- Mode should be import or export depending on if you're fetching from pearson or
- sending to them.
- """
+ option_list = BaseCommand.option_list + (
+ make_option('--mode',
+ action='store_true',
+ dest='mode',
+ default='both',
+ help='mode is import, export, or both'),
+ )
- def handle(self, *args):
- if len(args) < 1:
- raise CommandError('Usage is pearson {0}'.format(self.args))
+ def handle(self, **options):
+
+ if not settings.PEARSON:
+ raise CommandError('No PEARSON entries in auth/env.json.')
+
+ def import_pearson():
+ sftp(settings.PEARSON[SFTP_IMPORT], settings.PEARSON[LOCAL_IMPORT])
+ s3(settings.PEARSON[LOCAL_IMPORT], settings.PEARSON[BUCKET])
+ call_command('pearson_import', 'dest_from_settings=True')
+
+ def export_pearson():
+ call_command('pearson_export_ccd', 'dest_from_settings=True')
+ call_command('pearson_export_ead', 'dest_from_settings=True')
+ sftp(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[SFTP_EXPORT])
+ s3(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[BUCKET])
+
+ if options['mode'] == 'both':
+ export_pearson()
+ import_pearson()
+ elif options['mode'] == 'export':
+ export_pearson()
+ elif options['mode'] == 'import':
+ import_pearson()
+ else:
+ print("ERROR: Mode must be export or import.")
- for mode in args:
- if mode == 'export':
- sftp(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[SFTP_EXPORT])
- s3(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[BUCKET])
- elif mode == 'import':
- sftp(settings.PEARSON[SFTP_IMPORT], settings.PEARSON[LOCAL_IMPORT])
- s3(settings.PEARSON[LOCAL_IMPORT], settings.PEARSON[BUCKET])
- else:
- print("ERROR: Mode must be export or import.")
def sftp(files_from, files_to):
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='sftp'):
From 0555ebc4347c9eaa0943067386f55b3d0bf6d139 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Wed, 16 Jan 2013 12:04:53 -0500
Subject: [PATCH 10/82] Bunch of fixes to how I do if/else checks, fix a typo
in Command and repair the for filename part of sftp.
---
.../management/commands/pearson_export_cdd.py | 14 ++++++--------
.../management/commands/pearson_export_ead.py | 15 +++++++--------
.../management/commands/pearson_transfer.py | 6 +++---
3 files changed, 16 insertions(+), 19 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_export_cdd.py b/common/djangoapps/student/management/commands/pearson_export_cdd.py
index c2c13916ab..2655960ff5 100644
--- a/common/djangoapps/student/management/commands/pearson_export_cdd.py
+++ b/common/djangoapps/student/management/commands/pearson_export_cdd.py
@@ -44,7 +44,7 @@ class Command(BaseCommand):
default=False,
help='Retrieve the destination to export to from django? True/False'),
make_option('--destination',
- action='store_true',
+ action='store',
dest='destination',
default=None,
help='Where to store the exported files')
@@ -61,24 +61,22 @@ class Command(BaseCommand):
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
- if options['dest-from-settings'] is True:
- if settings.PEARSON[LOCAL_EXPORT]:
+ if 'dest-from-settings' in options:
+ if LOCAL_EXPORT in settings.PEARSON:
dest = settings.PEARSON[LOCAL_EXPORT]
else:
raise CommandError('--dest-from-settings was enabled but the'
'PEARSON[LOCAL_EXPORT] setting was not set.')
- elif options['destination']:
+ elif 'destination' in options:
dest = options['destination']
else:
- raise ComamndError('--destination or --dest-from-settings must be used')
+ raise CommandError('--destination or --dest-from-settings must be used')
if not os.path.isdir(dest):
os.makedirs(dest)
- destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
- else:
- destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
+ destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
# strings must be in latin-1 format. CSV parser will
# otherwise convert unicode objects to ascii.
diff --git a/common/djangoapps/student/management/commands/pearson_export_ead.py b/common/djangoapps/student/management/commands/pearson_export_ead.py
index 6e3149778d..7697c99e4a 100644
--- a/common/djangoapps/student/management/commands/pearson_export_ead.py
+++ b/common/djangoapps/student/management/commands/pearson_export_ead.py
@@ -30,7 +30,7 @@ class Command(BaseCommand):
default=False,
help='Retrieve the destination to export to from django? True/False'),
make_option('--destination',
- action='store_true',
+ action='store',
dest='destination',
default=None,
help='Where to store the exported files'),
@@ -55,22 +55,21 @@ class Command(BaseCommand):
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
- if options['dest-from-settings'] is True:
- if settings.PEARSON[LOCAL_EXPORT]:
+ if 'dest-from-settings' in options:
+ if LOCAL_EXPORT in settings.PEARSON:
dest = settings.PEARSON[LOCAL_EXPORT]
else:
raise CommandError('--dest-from-settings was enabled but the'
'PEARSON[LOCAL_EXPORT] setting was not set.')
- elif options['destination']:
+ elif destinations in options:
dest = options['destination']
else:
- raise ComamndError('--destination or --dest-from-settings must be used')
+ raise CommandError('--destination or --dest-from-settings must be used')
if not os.path.isdir(dest):
os.makedirs(dest)
- destfile = os.path.join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
- else:
- destfile = os.path.join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
+
+ destfile = os.path.join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
dump_all = kwargs['dump_all']
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index 1d04936216..7d4a2ead3f 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -14,7 +14,7 @@ class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--mode',
- action='store_true',
+ action='store',
dest='mode',
default='both',
help='mode is import, export, or both'),
@@ -55,11 +55,11 @@ class Command(BaseCommand):
password=settings.PEARSON[SFTP_PASSWORD])
sftp = paramiko.SFTPClient.from_transport(t)
if os.path.isdir(files_from):
- for file in os.listdir(files_from):
+ for filename in os.listdir(files_from):
sftp.put(files_from+'/'+filename,
files_to+'/'+filename)
else:
- for file in sftp.listdir(files_from):
+ for filename in sftp.listdir(files_from):
sftp.get(files_from+'/'+filename,
files_to+'/'+filename)
except:
From 3d5599c829fb8d7d06e89ce331b67fa6c419997d Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Wed, 16 Jan 2013 12:12:22 -0500
Subject: [PATCH 11/82] Further fixes to close the ssh connection explictly,
make both the default option if nothing is provided, and not bother passing
true when calling the subcommands.
---
.../management/commands/pearson_transfer.py | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index 7d4a2ead3f..d07f75d011 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -28,23 +28,21 @@ class Command(BaseCommand):
def import_pearson():
sftp(settings.PEARSON[SFTP_IMPORT], settings.PEARSON[LOCAL_IMPORT])
s3(settings.PEARSON[LOCAL_IMPORT], settings.PEARSON[BUCKET])
- call_command('pearson_import', 'dest_from_settings=True')
+ call_command('pearson_import', 'dest_from_settings')
def export_pearson():
- call_command('pearson_export_ccd', 'dest_from_settings=True')
- call_command('pearson_export_ead', 'dest_from_settings=True')
+ call_command('pearson_export_ccd', 'dest_from_settings')
+ call_command('pearson_export_ead', 'dest_from_settings')
sftp(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[SFTP_EXPORT])
s3(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[BUCKET])
- if options['mode'] == 'both':
- export_pearson()
- import_pearson()
- elif options['mode'] == 'export':
+ if options['mode'] == 'export':
export_pearson()
elif options['mode'] == 'import':
import_pearson()
else:
- print("ERROR: Mode must be export or import.")
+ export_pearson()
+ import_pearson()
def sftp(files_from, files_to):
@@ -62,6 +60,7 @@ class Command(BaseCommand):
for filename in sftp.listdir(files_from):
sftp.get(files_from+'/'+filename,
files_to+'/'+filename)
+ t.close()
except:
dog_http_api.event('pearson {0}'.format(mode),
'sftp uploading failed', alert_type='error')
From 5431332cecf5773f33b56856df2f41c0b0b6156f Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Wed, 16 Jan 2013 12:14:43 -0500
Subject: [PATCH 12/82] Fix up help.
---
.../student/management/commands/pearson_export_cdd.py | 2 +-
.../student/management/commands/pearson_export_ead.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_export_cdd.py b/common/djangoapps/student/management/commands/pearson_export_cdd.py
index 2655960ff5..2f68ccc7d1 100644
--- a/common/djangoapps/student/management/commands/pearson_export_cdd.py
+++ b/common/djangoapps/student/management/commands/pearson_export_cdd.py
@@ -42,7 +42,7 @@ class Command(BaseCommand):
action='store_true',
dest='dest-from-settings',
default=False,
- help='Retrieve the destination to export to from django? True/False'),
+ help='Retrieve the destination to export to from django.'),
make_option('--destination',
action='store',
dest='destination',
diff --git a/common/djangoapps/student/management/commands/pearson_export_ead.py b/common/djangoapps/student/management/commands/pearson_export_ead.py
index 7697c99e4a..0f81cb1df9 100644
--- a/common/djangoapps/student/management/commands/pearson_export_ead.py
+++ b/common/djangoapps/student/management/commands/pearson_export_ead.py
@@ -28,7 +28,7 @@ class Command(BaseCommand):
action='store_true',
dest='dest-from-settings',
default=False,
- help='Retrieve the destination to export to from django? True/False'),
+ help='Retrieve the destination to export to from django.'),
make_option('--destination',
action='store',
dest='destination',
From 482cefd246539169d3c0de8704af29ca6e3c5bf2 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Wed, 16 Jan 2013 13:37:33 -0500
Subject: [PATCH 13/82] Bunch of fixes to pep8 formatting, missing imports,
change of kwargs to options, quoting variables and a few other fixups.
---
.../management/commands/pearson_export_cdd.py | 27 +++++-----
.../management/commands/pearson_export_ead.py | 27 +++++-----
.../management/commands/pearson_transfer.py | 53 +++++++++++--------
3 files changed, 55 insertions(+), 52 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_export_cdd.py b/common/djangoapps/student/management/commands/pearson_export_cdd.py
index 2f68ccc7d1..14c652f11e 100644
--- a/common/djangoapps/student/management/commands/pearson_export_cdd.py
+++ b/common/djangoapps/student/management/commands/pearson_export_cdd.py
@@ -1,15 +1,16 @@
import csv
+import os
from collections import OrderedDict
from datetime import datetime
-from os.path import isdir
from optparse import make_option
from django.core.management.base import BaseCommand
from student.models import TestCenterUser
+
class Command(BaseCommand):
-
+
CSV_TO_MODEL_FIELDS = OrderedDict([
# Skipping optional field CandidateID
("ClientCandidateID", "client_candidate_id"),
@@ -34,7 +35,7 @@ class Command(BaseCommand):
("FAXCountryCode", "fax_country_code"),
("CompanyName", "company_name"),
# Skipping optional field CustomQuestion
- ("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
+ ("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
option_list = BaseCommand.option_list + (
@@ -55,29 +56,28 @@ class Command(BaseCommand):
# field
uploaded_at = datetime.utcnow()
- # if specified destination is an existing directory, then
+ # if specified destination is an existing directory, then
# create a filename for it automatically. If it doesn't exist,
# then we will create the directory.
# Name will use timestamp -- this is UTC, so it will look funny,
- # but it should at least be consistent with the other timestamps
+ # but it should at least be consistent with the other timestamps
# used in the system.
if 'dest-from-settings' in options:
- if LOCAL_EXPORT in settings.PEARSON:
- dest = settings.PEARSON[LOCAL_EXPORT]
+ if 'LOCAL_EXPORT' in settings.PEARSON:
+ dest = settings.PEARSON['LOCAL_EXPORT']
else:
raise CommandError('--dest-from-settings was enabled but the'
- 'PEARSON[LOCAL_EXPORT] setting was not set.')
+ 'PEARSON[LOCAL_EXPORT] setting was not set.')
elif 'destination' in options:
dest = options['destination']
else:
raise CommandError('--destination or --dest-from-settings must be used')
-
if not os.path.isdir(dest):
os.makedirs(dest)
destfile = os.path.join(dest, uploaded_at.strftime("cdd-%Y%m%d-%H%M%S.dat"))
-
+
# strings must be in latin-1 format. CSV parser will
# otherwise convert unicode objects to ascii.
def ensure_encoding(value):
@@ -85,8 +85,8 @@ class Command(BaseCommand):
return value.encode('iso-8859-1')
else:
return value
-
- dump_all = kwargs['dump_all']
+
+ dump_all = options['dump_all']
with open(destfile, "wb") as outfile:
writer = csv.DictWriter(outfile,
@@ -104,6 +104,3 @@ class Command(BaseCommand):
writer.writerow(record)
tcu.uploaded_at = uploaded_at
tcu.save()
-
-
-
diff --git a/common/djangoapps/student/management/commands/pearson_export_ead.py b/common/djangoapps/student/management/commands/pearson_export_ead.py
index 0f81cb1df9..9368ac5ddf 100644
--- a/common/djangoapps/student/management/commands/pearson_export_ead.py
+++ b/common/djangoapps/student/management/commands/pearson_export_ead.py
@@ -1,15 +1,16 @@
import csv
+import os
from collections import OrderedDict
from datetime import datetime
-from os.path import isdir, join
from optparse import make_option
from django.core.management.base import BaseCommand
from student.models import TestCenterRegistration
+
class Command(BaseCommand):
-
+
CSV_TO_MODEL_FIELDS = OrderedDict([
('AuthorizationTransactionType', 'authorization_transaction_type'),
('AuthorizationID', 'authorization_id'),
@@ -20,7 +21,7 @@ class Command(BaseCommand):
('Accommodations', 'accommodation_code'),
('EligibilityApptDateFirst', 'eligibility_appointment_date_first'),
('EligibilityApptDateLast', 'eligibility_appointment_date_last'),
- ("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
+ ("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
option_list = BaseCommand.option_list + (
@@ -37,11 +38,11 @@ class Command(BaseCommand):
make_option('--dump_all',
action='store_true',
dest='dump_all',
- ),
+ ),
make_option('--force_add',
action='store_true',
dest='force_add',
- ),
+ ),
)
def handle(self, **options):
@@ -56,12 +57,12 @@ class Command(BaseCommand):
# but it should at least be consistent with the other timestamps
# used in the system.
if 'dest-from-settings' in options:
- if LOCAL_EXPORT in settings.PEARSON:
- dest = settings.PEARSON[LOCAL_EXPORT]
+ if 'LOCAL_EXPORT' in settings.PEARSON:
+ dest = settings.PEARSON['LOCAL_EXPORT']
else:
raise CommandError('--dest-from-settings was enabled but the'
- 'PEARSON[LOCAL_EXPORT] setting was not set.')
- elif destinations in options:
+ 'PEARSON[LOCAL_EXPORT] setting was not set.')
+ elif 'destinations' in options:
dest = options['destination']
else:
raise CommandError('--destination or --dest-from-settings must be used')
@@ -71,7 +72,7 @@ class Command(BaseCommand):
destfile = os.path.join(dest, uploaded_at.strftime("ead-%Y%m%d-%H%M%S.dat"))
- dump_all = kwargs['dump_all']
+ dump_all = options['dump_all']
with open(destfile, "wb") as outfile:
writer = csv.DictWriter(outfile,
@@ -88,13 +89,9 @@ class Command(BaseCommand):
record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S")
record["EligibilityApptDateFirst"] = record["EligibilityApptDateFirst"].strftime("%Y/%m/%d")
record["EligibilityApptDateLast"] = record["EligibilityApptDateLast"].strftime("%Y/%m/%d")
- if kwargs['force_add']:
+ if options['force_add']:
record['AuthorizationTransactionType'] = 'Add'
writer.writerow(record)
tcr.uploaded_at = uploaded_at
tcr.save()
-
-
-
-
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index d07f75d011..8183bf3c05 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -1,11 +1,10 @@
from optparse import make_option
-from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
-import re
from dogapi import dog_http_api, dog_stats_api
import paramiko
import boto
+import os
dog_http_api.api_key = settings.DATADOG_API
@@ -25,16 +24,26 @@ class Command(BaseCommand):
if not settings.PEARSON:
raise CommandError('No PEARSON entries in auth/env.json.')
+ for value in ['LOCAL_IMPORT', 'SFTP_IMPORT', 'BUCKET', 'LOCAL_EXPORT',
+ 'SFTP_EXPORT']:
+ if value not in settings.PEARSON:
+ raise CommandError('No entry in the PEARSON settings'
+ '(env/auth.json) for {0}'.format(value))
+
def import_pearson():
- sftp(settings.PEARSON[SFTP_IMPORT], settings.PEARSON[LOCAL_IMPORT])
- s3(settings.PEARSON[LOCAL_IMPORT], settings.PEARSON[BUCKET])
+ sftp(settings.PEARSON['SFTP_IMPORT'],
+ settings.PEARSON['LOCAL_IMPORT'], options['mode'])
+ s3(settings.PEARSON['LOCAL_IMPORT'],
+ settings.PEARSON['BUCKET'], options['mode'])
call_command('pearson_import', 'dest_from_settings')
def export_pearson():
call_command('pearson_export_ccd', 'dest_from_settings')
call_command('pearson_export_ead', 'dest_from_settings')
- sftp(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[SFTP_EXPORT])
- s3(settings.PEARSON[LOCAL_EXPORT], settings.PEARSON[BUCKET])
+ sftp(settings.PEARSON['LOCAL_EXPORT'],
+ settings.PEARSON['SFTP_EXPORT'], options['mode'])
+ s3(settings.PEARSON['LOCAL_EXPORT'],
+ settings.PEARSON['BUCKET'], options['mode'])
if options['mode'] == 'export':
export_pearson()
@@ -44,44 +53,44 @@ class Command(BaseCommand):
export_pearson()
import_pearson()
-
- def sftp(files_from, files_to):
+ def sftp(files_from, files_to, mode):
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='sftp'):
try:
- t = paramiko.Transport((hostname, 22))
- t.connect(username=settings.PEARSON[SFTP_USERNAME],
- password=settings.PEARSON[SFTP_PASSWORD])
+ t = paramiko.Transport((settings.PEARSON['SFTP_HOSTNAME'], 22))
+ t.connect(username=settings.PEARSON['SFTP_USERNAME'],
+ password=settings.PEARSON['SFTP_PASSWORD'])
sftp = paramiko.SFTPClient.from_transport(t)
if os.path.isdir(files_from):
for filename in os.listdir(files_from):
- sftp.put(files_from+'/'+filename,
- files_to+'/'+filename)
+ sftp.put(files_from + '/' + filename,
+ files_to + '/' + filename)
else:
for filename in sftp.listdir(files_from):
- sftp.get(files_from+'/'+filename,
- files_to+'/'+filename)
+ sftp.get(files_from + '/' + filename,
+ files_to + '/' + filename)
t.close()
except:
dog_http_api.event('pearson {0}'.format(mode),
- 'sftp uploading failed', alert_type='error')
+ 'sftp uploading failed',
+ alert_type='error')
raise
- def s3(files_from, bucket):
+ def s3(files_from, bucket, mode):
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='s3'):
try:
- for filename in os.listdir(files):
- upload_file_to_s3(bucket, files_from+'/'+filename)
+ for filename in os.listdir(files_from):
+ upload_file_to_s3(bucket, files_from + '/' + filename)
except:
- dog_http_api.event('pearson {0}'.format(mode), 's3 archiving failed')
+ dog_http_api.event('pearson {0}'.format(mode),
+ 's3 archiving failed')
raise
-
def upload_file_to_s3(bucket, filename):
"""
Upload file to S3
"""
s3 = boto.connect_s3(settings.AWS_ACCESS_KEY_ID,
- settings.AWS_SECRET_ACCESS_KEY)
+ settings.AWS_SECRET_ACCESS_KEY)
from boto.s3.key import Key
b = s3.get_bucket(bucket)
k = Key(b)
From 46e9a8f6ac58c1a79ee0097cd6fbfd829eae9b1d Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 10:09:11 -0500
Subject: [PATCH 14/82] Add help string.
---
.../student/management/commands/pearson_transfer.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index 8183bf3c05..4888806c73 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -10,6 +10,14 @@ dog_http_api.api_key = settings.DATADOG_API
class Command(BaseCommand):
+ help = """
+ This command handles the importing and exporting of student records for
+ Pearson. It uses some other Django commands to export and import the
+ files and then uploads over SFTP to pearson and stuffs the entry in an
+ S3 bucket for archive purposes.
+
+ Usage: django-admin.py pearson-transfer --mode [import|export|both]
+ """
option_list = BaseCommand.option_list + (
make_option('--mode',
From 5e76c9d033960221bb167d8e32caeae5898d93f5 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Thu, 17 Jan 2013 10:43:20 -0500
Subject: [PATCH 15/82] add pearson_import_conf_zip command
---
.../commands/pearson_import_conf_zip.py | 108 ++++++++++++++++++
1 file changed, 108 insertions(+)
create mode 100644 common/djangoapps/student/management/commands/pearson_import_conf_zip.py
diff --git a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
new file mode 100644
index 0000000000..7b69a29dc2
--- /dev/null
+++ b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
@@ -0,0 +1,108 @@
+import csv
+
+from zipfile import ZipFile, is_zipfile
+from time import strptime, strftime
+
+from collections import OrderedDict
+from datetime import datetime
+from os.path import isdir
+from optparse import make_option
+
+from django.core.management.base import BaseCommand, CommandError
+
+from student.models import TestCenterUser, TestCenterRegistration
+
+class Command(BaseCommand):
+
+
+ args = ''
+ help = """
+ Import Pearson confirmation files and update TestCenterUser and TestCenterRegistration tables
+ with status.
+ """
+ def handle(self, *args, **kwargs):
+ if len(args) < 1:
+ print Command.help
+ return
+
+ source_zip = args[0]
+ if not is_zipfile(source_zip):
+ raise CommandError("Input file is not a zipfile: \"{}\"".format(source_zip))
+
+ # loop through all files in zip, and process them based on filename prefix:
+ with ZipFile(source_zip, 'r') as zipfile:
+ for fileinfo in zipfile.infolist():
+ with zipfile.open(fileinfo) as zipentry:
+ if fileinfo.filename.startswith("eac-"):
+ self.process_eac(zipentry)
+ elif fileinfo.filename.startswith("vcdc-"):
+ self.process_vcdc(zipentry)
+ else:
+ raise CommandError("Unrecognized confirmation file type \"{}\" in confirmation zip file \"{}\"".format(fileinfo.filename, zipfile))
+
+ def process_eac(self, eacfile):
+ print "processing eac"
+ reader = csv.DictReader(eacfile, delimiter="\t")
+ for row in reader:
+ client_authorization_id = row['ClientAuthorizationID']
+ if not client_authorization_id:
+ if row['Status'] == 'Error':
+ print "Error in EAD file processing ({}): {}".format(row['Date'], row['Message'])
+ else:
+ print "Encountered bad record: {}".format(row)
+ else:
+ try:
+ registration = TestCenterRegistration.objects.get(client_authorization_id=client_authorization_id)
+ print "Found authorization record for user {}".format(registration.testcenter_user.user.username)
+ # now update the record:
+ registration.upload_status = row['Status']
+ registration.upload_error_message = row['Message']
+ try:
+ registration.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
+ except ValueError as ve:
+ print "Bad Date value found for {}: message {}".format(client_authorization_id, ve)
+ # store the authorization Id if one is provided. (For debugging)
+ if row['AuthorizationID']:
+ try:
+ registration.authorization_id = int(row['AuthorizationID'])
+ except ValueError as ve:
+ print "Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve)
+
+ registration.confirmed_at = datetime.utcnow()
+ registration.save()
+ except TestCenterRegistration.DoesNotExist:
+ print " Failed to find record for client_auth_id {}".format(client_authorization_id)
+
+
+ def process_vcdc(self, vcdcfile):
+ print "processing vcdc"
+ reader = csv.DictReader(vcdcfile, delimiter="\t")
+ for row in reader:
+ client_candidate_id = row['ClientCandidateID']
+ if not client_candidate_id:
+ if row['Status'] == 'Error':
+ print "Error in CDD file processing ({}): {}".format(row['Date'], row['Message'])
+ else:
+ print "Encountered bad record: {}".format(row)
+ else:
+ try:
+ tcuser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
+ print "Found demographics record for user {}".format(tcuser.user.username)
+ # now update the record:
+ tcuser.upload_status = row['Status']
+ tcuser.upload_error_message = row['Message']
+ try:
+ tcuser.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
+ except ValueError as ve:
+ print "Bad Date value found for {}: message {}".format(client_candidate_id, ve)
+ # store the candidate Id if one is provided. (For debugging)
+ if row['CandidateID']:
+ try:
+ tcuser.candidate_id = int(row['CandidateID'])
+ except ValueError as ve:
+ print "Bad CandidateID value found for {}: message {}".format(client_candidate_id, ve)
+ tcuser.confirmed_at = datetime.utcnow()
+ tcuser.save()
+ except TestCenterUser.DoesNotExist:
+ print " Failed to find record for client_candidate_id {}".format(client_candidate_id)
+
From 333f2e5167556043e2f09de06aa297ab08ee50de Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 10:48:16 -0500
Subject: [PATCH 16/82] Fix ccd->cdd, I typed ccd so many times while working
on this code. I am bad at typing!
---
.../djangoapps/student/management/commands/pearson_transfer.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index 4888806c73..57401897dc 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -46,7 +46,7 @@ class Command(BaseCommand):
call_command('pearson_import', 'dest_from_settings')
def export_pearson():
- call_command('pearson_export_ccd', 'dest_from_settings')
+ call_command('pearson_export_cdd', 'dest_from_settings')
call_command('pearson_export_ead', 'dest_from_settings')
sftp(settings.PEARSON['LOCAL_EXPORT'],
settings.PEARSON['SFTP_EXPORT'], options['mode'])
From 3a4091b798df7d4c1f7c9f1f3686eac77235cba3 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 13:54:43 -0500
Subject: [PATCH 17/82] Tweaks to enable datadog error events as well as some
pep8 tidyup vim was shouting at me about.
---
.../commands/pearson_import_conf_zip.py | 55 ++++++++++---------
1 file changed, 30 insertions(+), 25 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
index 7b69a29dc2..74037b708f 100644
--- a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
+++ b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
@@ -7,19 +7,25 @@ from collections import OrderedDict
from datetime import datetime
from os.path import isdir
from optparse import make_option
+from dogapi import dog_http_api, dog_stats_api
from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser, TestCenterRegistration
+
class Command(BaseCommand):
-
-
+
+ dog_http_api.api_key = settings.DATADOG_API
args = ''
help = """
- Import Pearson confirmation files and update TestCenterUser and TestCenterRegistration tables
- with status.
+ Import Pearson confirmation files and update TestCenterUser
+ and TestCenterRegistration tables with status.
"""
+
+ def datadog_error(string):
+ dog_http_api.event("Pearson Import", string, alert_type='error')
+
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
@@ -27,8 +33,8 @@ class Command(BaseCommand):
source_zip = args[0]
if not is_zipfile(source_zip):
- raise CommandError("Input file is not a zipfile: \"{}\"".format(source_zip))
-
+ raise CommandError("Input file is not a zipfile: \"{}\"".format(source_zip))
+
# loop through all files in zip, and process them based on filename prefix:
with ZipFile(source_zip, 'r') as zipfile:
for fileinfo in zipfile.infolist():
@@ -39,70 +45,69 @@ class Command(BaseCommand):
self.process_vcdc(zipentry)
else:
raise CommandError("Unrecognized confirmation file type \"{}\" in confirmation zip file \"{}\"".format(fileinfo.filename, zipfile))
-
- def process_eac(self, eacfile):
+
+ def process_eac(self, eacfile):
print "processing eac"
reader = csv.DictReader(eacfile, delimiter="\t")
for row in reader:
client_authorization_id = row['ClientAuthorizationID']
if not client_authorization_id:
if row['Status'] == 'Error':
- print "Error in EAD file processing ({}): {}".format(row['Date'], row['Message'])
+ Command.datadog_error("Error in EAD file processing ({}): {}".format(row['Date'], row['Message']))
else:
- print "Encountered bad record: {}".format(row)
+ Command.datadog_error("Encountered bad record: {}".format(row))
else:
try:
registration = TestCenterRegistration.objects.get(client_authorization_id=client_authorization_id)
- print "Found authorization record for user {}".format(registration.testcenter_user.user.username)
+ Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username))
# now update the record:
registration.upload_status = row['Status']
registration.upload_error_message = row['Message']
try:
registration.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
except ValueError as ve:
- print "Bad Date value found for {}: message {}".format(client_authorization_id, ve)
+ Command.datadog_error("Bad Date value found for {}: message {}".format(client_authorization_id, ve))
# store the authorization Id if one is provided. (For debugging)
if row['AuthorizationID']:
try:
registration.authorization_id = int(row['AuthorizationID'])
except ValueError as ve:
- print "Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve)
-
+ Command.datadog_error("Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve))
+
registration.confirmed_at = datetime.utcnow()
registration.save()
except TestCenterRegistration.DoesNotExist:
- print " Failed to find record for client_auth_id {}".format(client_authorization_id)
+ Command.datadog_error("Failed to find record for client_auth_id {}".format(client_authorization_id))
-
- def process_vcdc(self, vcdcfile):
+ def process_vcdc(self, vcdcfile):
print "processing vcdc"
reader = csv.DictReader(vcdcfile, delimiter="\t")
for row in reader:
client_candidate_id = row['ClientCandidateID']
if not client_candidate_id:
if row['Status'] == 'Error':
- print "Error in CDD file processing ({}): {}".format(row['Date'], row['Message'])
+ Command.datadog_error("Error in CDD file processing ({}): {}".format(row['Date'], row['Message']))
else:
- print "Encountered bad record: {}".format(row)
+ Command.datadog_error("Encountered bad record: {}".format(row))
else:
try:
tcuser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
- print "Found demographics record for user {}".format(tcuser.user.username)
+ Command.datadog_error("Found demographics record for user {}".format(tcuser.user.username))
# now update the record:
tcuser.upload_status = row['Status']
- tcuser.upload_error_message = row['Message']
+ tcuser.upload_error_message = row['Message']
try:
tcuser.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
except ValueError as ve:
- print "Bad Date value found for {}: message {}".format(client_candidate_id, ve)
+ Command.datadog_error("Bad Date value found for {}: message {}".format(client_candidate_id, ve))
# store the candidate Id if one is provided. (For debugging)
if row['CandidateID']:
try:
tcuser.candidate_id = int(row['CandidateID'])
except ValueError as ve:
- print "Bad CandidateID value found for {}: message {}".format(client_candidate_id, ve)
+ Command.datadog_error("Bad CandidateID value found for {}: message {}".format(client_candidate_id, ve))
tcuser.confirmed_at = datetime.utcnow()
tcuser.save()
except TestCenterUser.DoesNotExist:
- print " Failed to find record for client_candidate_id {}".format(client_candidate_id)
-
+ Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id))
+
From 05b36bdd092de2294381fb2fcbbc2fbb03b63ed6 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 15:43:37 -0500
Subject: [PATCH 18/82] Add tags to the datadog event which will be set to the
appropriate filename so we can see the cause of failures.
---
.../commands/pearson_import_conf_zip.py | 32 ++++++++++---------
1 file changed, 17 insertions(+), 15 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
index 74037b708f..a491a73868 100644
--- a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
+++ b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
@@ -23,8 +23,8 @@ class Command(BaseCommand):
and TestCenterRegistration tables with status.
"""
- def datadog_error(string):
- dog_http_api.event("Pearson Import", string, alert_type='error')
+ def datadog_error(string, tags):
+ dog_http_api.event("Pearson Import", string, alert_type='error', tags=tags)
def handle(self, *args, **kwargs):
if len(args) < 1:
@@ -44,7 +44,9 @@ class Command(BaseCommand):
elif fileinfo.filename.startswith("vcdc-"):
self.process_vcdc(zipentry)
else:
- raise CommandError("Unrecognized confirmation file type \"{}\" in confirmation zip file \"{}\"".format(fileinfo.filename, zipfile))
+ error = "Unrecognized confirmation file type\"{}\" in confirmation zip file \"{}\"".format(fileinfo.filename, zipfile)
+ Command.datadog_error(error, source_zip)
+ raise CommandError(error)
def process_eac(self, eacfile):
print "processing eac"
@@ -53,31 +55,31 @@ class Command(BaseCommand):
client_authorization_id = row['ClientAuthorizationID']
if not client_authorization_id:
if row['Status'] == 'Error':
- Command.datadog_error("Error in EAD file processing ({}): {}".format(row['Date'], row['Message']))
+ Command.datadog_error("Error in EAD file processing ({}): {}".format(row['Date'], row['Message']), eacfile)
else:
- Command.datadog_error("Encountered bad record: {}".format(row))
+ Command.datadog_error("Encountered bad record: {}".format(row), eacfile)
else:
try:
registration = TestCenterRegistration.objects.get(client_authorization_id=client_authorization_id)
- Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username))
+ Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username), eacfile)
# now update the record:
registration.upload_status = row['Status']
registration.upload_error_message = row['Message']
try:
registration.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
except ValueError as ve:
- Command.datadog_error("Bad Date value found for {}: message {}".format(client_authorization_id, ve))
+ Command.datadog_error("Bad Date value found for {}: message {}".format(client_authorization_id, ve), eacfile)
# store the authorization Id if one is provided. (For debugging)
if row['AuthorizationID']:
try:
registration.authorization_id = int(row['AuthorizationID'])
except ValueError as ve:
- Command.datadog_error("Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve))
+ Command.datadog_error("Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve), eacfile)
registration.confirmed_at = datetime.utcnow()
registration.save()
except TestCenterRegistration.DoesNotExist:
- Command.datadog_error("Failed to find record for client_auth_id {}".format(client_authorization_id))
+ Command.datadog_error("Failed to find record for client_auth_id {}".format(client_authorization_id), eacfile)
def process_vcdc(self, vcdcfile):
print "processing vcdc"
@@ -86,28 +88,28 @@ class Command(BaseCommand):
client_candidate_id = row['ClientCandidateID']
if not client_candidate_id:
if row['Status'] == 'Error':
- Command.datadog_error("Error in CDD file processing ({}): {}".format(row['Date'], row['Message']))
+ Command.datadog_error("Error in CDD file processing ({}): {}".format(row['Date'], row['Message']), vcdcfile)
else:
- Command.datadog_error("Encountered bad record: {}".format(row))
+ Command.datadog_error("Encountered bad record: {}".format(row), vcdcfile)
else:
try:
tcuser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
- Command.datadog_error("Found demographics record for user {}".format(tcuser.user.username))
+ Command.datadog_error("Found demographics record for user {}".format(tcuser.user.username), vcdcfile)
# now update the record:
tcuser.upload_status = row['Status']
tcuser.upload_error_message = row['Message']
try:
tcuser.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
except ValueError as ve:
- Command.datadog_error("Bad Date value found for {}: message {}".format(client_candidate_id, ve))
+ Command.datadog_error("Bad Date value found for {}: message {}".format(client_candidate_id, ve), vcdcfile)
# store the candidate Id if one is provided. (For debugging)
if row['CandidateID']:
try:
tcuser.candidate_id = int(row['CandidateID'])
except ValueError as ve:
- Command.datadog_error("Bad CandidateID value found for {}: message {}".format(client_candidate_id, ve))
+ Command.datadog_error("Bad CandidateID value found for {}: message {}".format(client_candidate_id, ve), vcdcfile)
tcuser.confirmed_at = datetime.utcnow()
tcuser.save()
except TestCenterUser.DoesNotExist:
- Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id))
+ Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id), vcdcfile)
From 3aacba1f63b48b6d72883c2fd0a8b56d26573cc1 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 15:44:08 -0500
Subject: [PATCH 19/82] Run over each file and run the import. We could
probably do this as a try/except and not delete if the output of the import
failed but it may be simply easier to refetch those files from the S3 backup
and try again.
---
.../student/management/commands/pearson_transfer.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index 57401897dc..0037231a38 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -43,7 +43,9 @@ class Command(BaseCommand):
settings.PEARSON['LOCAL_IMPORT'], options['mode'])
s3(settings.PEARSON['LOCAL_IMPORT'],
settings.PEARSON['BUCKET'], options['mode'])
- call_command('pearson_import', 'dest_from_settings')
+ for file in os.listdir(settings.PEARSON['LOCAL_IMPORT']):
+ call_command('pearson_import_conf_zip', 'dest_from_settings')
+ os.remove(file)
def export_pearson():
call_command('pearson_export_cdd', 'dest_from_settings')
From 274cb8d865f00734e383a17ae3bdc29883acae45 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 15:50:37 -0500
Subject: [PATCH 20/82] Actually call the appropriate file.
---
.../djangoapps/student/management/commands/pearson_transfer.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index 0037231a38..aede8fe92d 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -44,7 +44,8 @@ class Command(BaseCommand):
s3(settings.PEARSON['LOCAL_IMPORT'],
settings.PEARSON['BUCKET'], options['mode'])
for file in os.listdir(settings.PEARSON['LOCAL_IMPORT']):
- call_command('pearson_import_conf_zip', 'dest_from_settings')
+ call_command('pearson_import_conf_zip',
+ settings.PEARSON['LOCAL_IMPORT'] + '/' + file)
os.remove(file)
def export_pearson():
From d0ecae30d7edaa64ae24b383c532a34faebd03d3 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 16:24:44 -0500
Subject: [PATCH 21/82] Fix datadog logging to use .name on the file objects
and add an additional logging line.
---
.../commands/pearson_import_conf_zip.py | 26 ++++++++++---------
.../management/commands/pearson_transfer.py | 21 +++++++++------
2 files changed, 27 insertions(+), 20 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
index a491a73868..fa9741dc68 100644
--- a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
+++ b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
@@ -33,7 +33,9 @@ class Command(BaseCommand):
source_zip = args[0]
if not is_zipfile(source_zip):
- raise CommandError("Input file is not a zipfile: \"{}\"".format(source_zip))
+ error = "Input file is not a zipfile: \"{}\"".format(source_zip)
+ Command.datadog_error(error, source_zip)
+ raise CommandError(error)
# loop through all files in zip, and process them based on filename prefix:
with ZipFile(source_zip, 'r') as zipfile:
@@ -55,9 +57,9 @@ class Command(BaseCommand):
client_authorization_id = row['ClientAuthorizationID']
if not client_authorization_id:
if row['Status'] == 'Error':
- Command.datadog_error("Error in EAD file processing ({}): {}".format(row['Date'], row['Message']), eacfile)
+ Command.datadog_error("Error in EAD file processing ({}): {}".format(row['Date'], row['Message']), eacfile.name)
else:
- Command.datadog_error("Encountered bad record: {}".format(row), eacfile)
+ Command.datadog_error("Encountered bad record: {}".format(row), eacfile.name)
else:
try:
registration = TestCenterRegistration.objects.get(client_authorization_id=client_authorization_id)
@@ -68,18 +70,18 @@ class Command(BaseCommand):
try:
registration.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
except ValueError as ve:
- Command.datadog_error("Bad Date value found for {}: message {}".format(client_authorization_id, ve), eacfile)
+ Command.datadog_error("Bad Date value found for {}: message {}".format(client_authorization_id, ve), eacfile.name)
# store the authorization Id if one is provided. (For debugging)
if row['AuthorizationID']:
try:
registration.authorization_id = int(row['AuthorizationID'])
except ValueError as ve:
- Command.datadog_error("Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve), eacfile)
+ Command.datadog_error("Bad AuthorizationID value found for {}: message {}".format(client_authorization_id, ve), eacfile.name)
registration.confirmed_at = datetime.utcnow()
registration.save()
except TestCenterRegistration.DoesNotExist:
- Command.datadog_error("Failed to find record for client_auth_id {}".format(client_authorization_id), eacfile)
+ Command.datadog_error("Failed to find record for client_auth_id {}".format(client_authorization_id), eacfile.name)
def process_vcdc(self, vcdcfile):
print "processing vcdc"
@@ -88,28 +90,28 @@ class Command(BaseCommand):
client_candidate_id = row['ClientCandidateID']
if not client_candidate_id:
if row['Status'] == 'Error':
- Command.datadog_error("Error in CDD file processing ({}): {}".format(row['Date'], row['Message']), vcdcfile)
+ Command.datadog_error("Error in CDD file processing ({}): {}".format(row['Date'], row['Message']), vcdcfile.name)
else:
- Command.datadog_error("Encountered bad record: {}".format(row), vcdcfile)
+ Command.datadog_error("Encountered bad record: {}".format(row), vcdcfile.name)
else:
try:
tcuser = TestCenterUser.objects.get(client_candidate_id=client_candidate_id)
- Command.datadog_error("Found demographics record for user {}".format(tcuser.user.username), vcdcfile)
+ Command.datadog_error("Found demographics record for user {}".format(tcuser.user.username), vcdcfile.name)
# now update the record:
tcuser.upload_status = row['Status']
tcuser.upload_error_message = row['Message']
try:
tcuser.processed_at = strftime('%Y-%m-%d %H:%M:%S', strptime(row['Date'], '%Y/%m/%d %H:%M:%S'))
except ValueError as ve:
- Command.datadog_error("Bad Date value found for {}: message {}".format(client_candidate_id, ve), vcdcfile)
+ Command.datadog_error("Bad Date value found for {}: message {}".format(client_candidate_id, ve), vcdcfile.name)
# store the candidate Id if one is provided. (For debugging)
if row['CandidateID']:
try:
tcuser.candidate_id = int(row['CandidateID'])
except ValueError as ve:
- Command.datadog_error("Bad CandidateID value found for {}: message {}".format(client_candidate_id, ve), vcdcfile)
+ Command.datadog_error("Bad CandidateID value found for {}: message {}".format(client_candidate_id, ve), vcdcfile.name)
tcuser.confirmed_at = datetime.utcnow()
tcuser.save()
except TestCenterUser.DoesNotExist:
- Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id), vcdcfile)
+ Command.datadog_error(" Failed to find record for client_candidate_id {}".format(client_candidate_id), vcdcfile.name)
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index aede8fe92d..5f126a24f0 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -39,14 +39,18 @@ class Command(BaseCommand):
'(env/auth.json) for {0}'.format(value))
def import_pearson():
- sftp(settings.PEARSON['SFTP_IMPORT'],
- settings.PEARSON['LOCAL_IMPORT'], options['mode'])
- s3(settings.PEARSON['LOCAL_IMPORT'],
- settings.PEARSON['BUCKET'], options['mode'])
- for file in os.listdir(settings.PEARSON['LOCAL_IMPORT']):
- call_command('pearson_import_conf_zip',
- settings.PEARSON['LOCAL_IMPORT'] + '/' + file)
- os.remove(file)
+ try:
+ sftp(settings.PEARSON['SFTP_IMPORT'],
+ settings.PEARSON['LOCAL_IMPORT'], options['mode'])
+ s3(settings.PEARSON['LOCAL_IMPORT'],
+ settings.PEARSON['BUCKET'], options['mode'])
+ except Exception as e:
+ dog_http_api.event('Pearson Import failure', str(e))
+ else:
+ for file in os.listdir(settings.PEARSON['LOCAL_IMPORT']):
+ call_command('pearson_import_conf_zip',
+ settings.PEARSON['LOCAL_IMPORT'] + '/' + file)
+ os.remove(file)
def export_pearson():
call_command('pearson_export_cdd', 'dest_from_settings')
@@ -79,6 +83,7 @@ class Command(BaseCommand):
for filename in sftp.listdir(files_from):
sftp.get(files_from + '/' + filename,
files_to + '/' + filename)
+ sftp.remove(files_from + '/' + filename)
t.close()
except:
dog_http_api.event('pearson {0}'.format(mode),
From 491dd408aa7193553e8d5f38e7386e150d7f3e7c Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 16:31:21 -0500
Subject: [PATCH 22/82] Add dogapi for datadog.
---
requirements.txt | 1 +
1 file changed, 1 insertion(+)
diff --git a/requirements.txt b/requirements.txt
index fa4688b711..7e4d913c30 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -59,3 +59,4 @@ Shapely==1.2.16
ipython==0.13.1
xmltodict==0.4.1
paramiko==1.9.0
+dogapi==1.1.2
From 60ae54b2e15a14095af8c5c2bef5111ef57d9e06 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 16:32:34 -0500
Subject: [PATCH 23/82] Actually, lets use Cale's fixes.
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 7e4d913c30..b1d769e9c2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -59,4 +59,4 @@ Shapely==1.2.16
ipython==0.13.1
xmltodict==0.4.1
paramiko==1.9.0
-dogapi==1.1.2
+git+ssh://git@github.com/MITx/dogapi.git@003a4fc9#egg=dogapi%
From c5979f8efa749376b32f03b4c112c1a39fd514ca Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 17 Jan 2013 16:38:35 -0500
Subject: [PATCH 24/82] Stray %
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index b1d769e9c2..1b1384912b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -59,4 +59,4 @@ Shapely==1.2.16
ipython==0.13.1
xmltodict==0.4.1
paramiko==1.9.0
-git+ssh://git@github.com/MITx/dogapi.git@003a4fc9#egg=dogapi%
+git+ssh://git@github.com/MITx/dogapi.git@003a4fc9#egg=dogapi
From 7bcfc44b7141d0171dc3a372647d36a81689f4ae Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Thu, 17 Jan 2013 17:50:26 -0500
Subject: [PATCH 25/82] add first test shell
---
.../management/commands/test/__init__.py | 0
.../management/commands/test/test_pearson.py | 53 +++++++++++++++++++
2 files changed, 53 insertions(+)
create mode 100644 common/djangoapps/student/management/commands/test/__init__.py
create mode 100644 common/djangoapps/student/management/commands/test/test_pearson.py
diff --git a/common/djangoapps/student/management/commands/test/__init__.py b/common/djangoapps/student/management/commands/test/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/djangoapps/student/management/commands/test/test_pearson.py b/common/djangoapps/student/management/commands/test/test_pearson.py
new file mode 100644
index 0000000000..b7f24ce232
--- /dev/null
+++ b/common/djangoapps/student/management/commands/test/test_pearson.py
@@ -0,0 +1,53 @@
+'''
+Created on Jan 17, 2013
+
+@author: brian
+'''
+import logging
+
+from django.test import TestCase
+from student.models import User, TestCenterRegistration, TestCenterUser, unique_id_for_user
+from mock import Mock
+from datetime import datetime
+from django.core import management
+
+COURSE_1 = 'edX/toy/2012_Fall'
+COURSE_2 = 'edx/full/6.002_Spring_2012'
+
+log = logging.getLogger(__name__)
+
+class PearsonTestCase(TestCase):
+ '''
+ Base class for tests running Pearson-related commands
+ '''
+
+ def test_create_good_testcenter_user(self):
+ username = "rusty"
+# user = Mock(username=username)
+# # id = unique_id_for_user(user)
+# course = Mock(end_of_course_survey_url=survey_url)
+
+
+ newuser = User.objects.create_user(username, 'rusty@edx.org', 'fakepass')
+# newuser.first_name='Rusty'
+# newuser.last_name='Skids'
+# newuser.is_staff=True
+# newuser.is_active=True
+# newuser.is_superuser=True
+# newuser.last_login=datetime(2012, 1, 1)
+# newuser.date_joined=datetime(2011, 1, 1)
+
+# newuser.save(using='default')
+ options = {
+ 'first_name' : 'TestFirst',
+ 'last_name' : 'TestLast',
+ 'address_1' : 'Test Address',
+ 'city' : 'TestCity',
+ 'state' : 'Alberta',
+ 'postal_code' : 'A0B 1C2',
+ 'country' : 'CAN',
+ 'phone' : '252-1866',
+ 'phone_country_code' : '1',
+ }
+ management.call_command('pearson_make_tc_user', username, options)
+
\ No newline at end of file
From d30974b560e23e0a0f8e9c1ecd495c689f69c21d Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Fri, 18 Jan 2013 04:00:32 -0500
Subject: [PATCH 26/82] Get pearson export working in a unit test
---
.../management/commands/pearson_export_cdd.py | 5 +-
.../management/commands/pearson_export_ead.py | 1 +
.../management/commands/test/test_pearson.py | 84 ++++++++++++-------
common/djangoapps/student/models.py | 4 +
4 files changed, 62 insertions(+), 32 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_export_cdd.py b/common/djangoapps/student/management/commands/pearson_export_cdd.py
index 14c652f11e..ba43dd3bc0 100644
--- a/common/djangoapps/student/management/commands/pearson_export_cdd.py
+++ b/common/djangoapps/student/management/commands/pearson_export_cdd.py
@@ -4,6 +4,7 @@ from collections import OrderedDict
from datetime import datetime
from optparse import make_option
+from django.conf import settings
from django.core.management.base import BaseCommand
from student.models import TestCenterUser
@@ -86,7 +87,7 @@ class Command(BaseCommand):
else:
return value
- dump_all = options['dump_all']
+# dump_all = options['dump_all']
with open(destfile, "wb") as outfile:
writer = csv.DictWriter(outfile,
@@ -96,7 +97,7 @@ class Command(BaseCommand):
extrasaction='ignore')
writer.writeheader()
for tcu in TestCenterUser.objects.order_by('id'):
- if dump_all or tcu.needs_uploading:
+ if tcu.needs_uploading: # or dump_all
record = dict((csv_field, ensure_encoding(getattr(tcu, model_field)))
for csv_field, model_field
in Command.CSV_TO_MODEL_FIELDS.items())
diff --git a/common/djangoapps/student/management/commands/pearson_export_ead.py b/common/djangoapps/student/management/commands/pearson_export_ead.py
index 9368ac5ddf..492cba154b 100644
--- a/common/djangoapps/student/management/commands/pearson_export_ead.py
+++ b/common/djangoapps/student/management/commands/pearson_export_ead.py
@@ -4,6 +4,7 @@ from collections import OrderedDict
from datetime import datetime
from optparse import make_option
+from django.conf import settings
from django.core.management.base import BaseCommand
from student.models import TestCenterRegistration
diff --git a/common/djangoapps/student/management/commands/test/test_pearson.py b/common/djangoapps/student/management/commands/test/test_pearson.py
index b7f24ce232..e0ded22a1d 100644
--- a/common/djangoapps/student/management/commands/test/test_pearson.py
+++ b/common/djangoapps/student/management/commands/test/test_pearson.py
@@ -6,39 +6,18 @@ Created on Jan 17, 2013
import logging
from django.test import TestCase
-from student.models import User, TestCenterRegistration, TestCenterUser, unique_id_for_user
-from mock import Mock
-from datetime import datetime
+from student.models import User, TestCenterRegistration, TestCenterUser
+# This is stupid! Because I import a function with the word "test" in the name,
+# the unittest framework tries to run *it* as a test?! Crazy!
+from student.models import get_testcenter_registration as get_tc_registration
from django.core import management
-COURSE_1 = 'edX/toy/2012_Fall'
-COURSE_2 = 'edx/full/6.002_Spring_2012'
-
log = logging.getLogger(__name__)
-class PearsonTestCase(TestCase):
- '''
- Base class for tests running Pearson-related commands
- '''
- def test_create_good_testcenter_user(self):
- username = "rusty"
-# user = Mock(username=username)
-# # id = unique_id_for_user(user)
-# course = Mock(end_of_course_survey_url=survey_url)
-
-
- newuser = User.objects.create_user(username, 'rusty@edx.org', 'fakepass')
-# newuser.first_name='Rusty'
-# newuser.last_name='Skids'
-# newuser.is_staff=True
-# newuser.is_active=True
-# newuser.is_superuser=True
-# newuser.last_login=datetime(2012, 1, 1)
-# newuser.date_joined=datetime(2011, 1, 1)
-
-# newuser.save(using='default')
- options = {
+def create_tc_user(username):
+ user = User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
+ options = {
'first_name' : 'TestFirst',
'last_name' : 'TestLast',
'address_1' : 'Test Address',
@@ -49,5 +28,50 @@ class PearsonTestCase(TestCase):
'phone' : '252-1866',
'phone_country_code' : '1',
}
- management.call_command('pearson_make_tc_user', username, options)
-
\ No newline at end of file
+ management.call_command('pearson_make_tc_user', username, **options)
+ return TestCenterUser.objects.get(user=user)
+
+
+def create_tc_registration(username, course_id, exam_code, accommodation_code):
+
+ options = { 'exam_series_code' : exam_code,
+ 'eligibility_appointment_date_first' : '2013-01-01T00:00',
+ 'eligibility_appointment_date_last' : '2013-12-31T23:59',
+ 'accommodation_code' : accommodation_code,
+ }
+
+ management.call_command('pearson_make_tc_registration', username, course_id, **options)
+ user = User.objects.get(username=username)
+ registrations = get_tc_registration(user, course_id, exam_code)
+ return registrations[0]
+
+class PearsonTestCase(TestCase):
+ '''
+ Base class for tests running Pearson-related commands
+ '''
+
+ def test_create_good_testcenter_user(self):
+ testcenter_user = create_tc_user("test1")
+
+ def test_create_good_testcenter_registration(self):
+ username = 'test1'
+ course_id = 'org1/course1/term1'
+ exam_code = 'exam1'
+ accommodation_code = 'NONE'
+ testcenter_user = create_tc_user(username)
+ registration = create_tc_registration(username, course_id, exam_code, accommodation_code)
+
+ def test_export(self):
+ username = 'test1'
+ course_id = 'org1/course1/term1'
+ exam_code = 'exam1'
+ accommodation_code = 'NONE'
+ testcenter_user = create_tc_user(username)
+ registration = create_tc_registration(username, course_id, exam_code, accommodation_code)
+ output_dir = "./tmpOutput"
+ options = { 'destination' : output_dir }
+ with self.settings(PEARSON={ 'LOCAL_EXPORT' : output_dir }):
+ management.call_command('pearson_export_cdd', **options)
+ management.call_command('pearson_export_ead', **options)
+ # TODO: check that files were output....
+
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index f13a691215..c9cb94d81a 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -428,6 +428,10 @@ class TestCenterRegistration(models.Model):
# TODO: figure out if this should really go in the database (with a default value).
return 1
+ @property
+ def needs_uploading(self):
+ return self.uploaded_at is None or self.uploaded_at < self.user_updated_at
+
@classmethod
def create(cls, testcenter_user, exam, accommodation_request):
registration = cls(testcenter_user = testcenter_user)
From 553528cd1c900e654e0c1556c0afe26dbbe57267 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Fri, 18 Jan 2013 14:52:36 -0500
Subject: [PATCH 27/82] change get_testcenter_registration to
get_tc_registration, so it's not treated as a test.
---
.../management/commands/pearson_make_tc_registration.py | 6 +++---
.../student/management/commands/{test => tests}/__init__.py | 0
.../management/commands/{test => tests}/test_pearson.py | 2 +-
common/djangoapps/student/models.py | 2 +-
common/djangoapps/student/views.py | 6 +++---
5 files changed, 8 insertions(+), 8 deletions(-)
rename common/djangoapps/student/management/commands/{test => tests}/__init__.py (100%)
rename common/djangoapps/student/management/commands/{test => tests}/test_pearson.py (97%)
diff --git a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
index 81a478d19d..545c8cd8a8 100644
--- a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
+++ b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
@@ -4,7 +4,7 @@ from time import strftime
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
-from student.models import TestCenterUser, TestCenterRegistration, TestCenterRegistrationForm, get_testcenter_registration
+from student.models import TestCenterUser, TestCenterRegistration, TestCenterRegistrationForm, get_tc_registration
from student.views import course_from_id
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
@@ -134,7 +134,7 @@ class Command(BaseCommand):
# create and save the registration:
needs_updating = False
- registrations = get_testcenter_registration(student, course_id, exam_code)
+ registrations = get_tc_registration(student, course_id, exam_code)
if len(registrations) > 0:
registration = registrations[0]
for fieldname in UPDATE_FIELDS:
@@ -181,7 +181,7 @@ class Command(BaseCommand):
change_internal = False
if 'exam_series_code' in our_options:
exam_code = our_options['exam_series_code']
- registration = get_testcenter_registration(student, course_id, exam_code)[0]
+ registration = get_tc_registration(student, course_id, exam_code)[0]
for internal_field in [ 'upload_error_message', 'upload_status', 'authorization_id']:
if internal_field in our_options:
registration.__setattr__(internal_field, our_options[internal_field])
diff --git a/common/djangoapps/student/management/commands/test/__init__.py b/common/djangoapps/student/management/commands/tests/__init__.py
similarity index 100%
rename from common/djangoapps/student/management/commands/test/__init__.py
rename to common/djangoapps/student/management/commands/tests/__init__.py
diff --git a/common/djangoapps/student/management/commands/test/test_pearson.py b/common/djangoapps/student/management/commands/tests/test_pearson.py
similarity index 97%
rename from common/djangoapps/student/management/commands/test/test_pearson.py
rename to common/djangoapps/student/management/commands/tests/test_pearson.py
index e0ded22a1d..29f9120e98 100644
--- a/common/djangoapps/student/management/commands/test/test_pearson.py
+++ b/common/djangoapps/student/management/commands/tests/test_pearson.py
@@ -9,7 +9,7 @@ from django.test import TestCase
from student.models import User, TestCenterRegistration, TestCenterUser
# This is stupid! Because I import a function with the word "test" in the name,
# the unittest framework tries to run *it* as a test?! Crazy!
-from student.models import get_testcenter_registration as get_tc_registration
+from student.models import get_tc_registration
from django.core import management
log = logging.getLogger(__name__)
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index c9cb94d81a..84bbc76a80 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -547,7 +547,7 @@ class TestCenterRegistrationForm(ModelForm):
-def get_testcenter_registration(user, course_id, exam_series_code):
+def get_tc_registration(user, course_id, exam_series_code):
try:
tcu = TestCenterUser.objects.get(user=user)
except TestCenterUser.DoesNotExist:
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 61b49e6022..650f0a0280 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -31,7 +31,7 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente
TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange,
CourseEnrollment, unique_id_for_user,
- get_testcenter_registration)
+ get_tc_registration)
from certificates.models import CertificateStatuses, certificate_status_for_student
@@ -612,7 +612,7 @@ def exam_registration_info(user, course):
return None
exam_code = exam_info.exam_series_code
- registrations = get_testcenter_registration(user, course.id, exam_code)
+ registrations = get_tc_registration(user, course.id, exam_code)
if registrations:
registration = registrations[0]
else:
@@ -712,7 +712,7 @@ def create_exam_registration(request, post_override=None):
needs_saving = False
exam = course.current_test_center_exam
exam_code = exam.exam_series_code
- registrations = get_testcenter_registration(user, course_id, exam_code)
+ registrations = get_tc_registration(user, course_id, exam_code)
if registrations:
registration = registrations[0]
# NOTE: we do not bother to check here to see if the registration has changed,
From f4703b40cb7098dbe44b91f050d23ead8359464f Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Fri, 18 Jan 2013 18:34:51 -0500
Subject: [PATCH 28/82] add test-with-settings
---
.../management/commands/tests/test_pearson.py | 54 ++++++++++++++++---
1 file changed, 47 insertions(+), 7 deletions(-)
diff --git a/common/djangoapps/student/management/commands/tests/test_pearson.py b/common/djangoapps/student/management/commands/tests/test_pearson.py
index 29f9120e98..fd9eb31631 100644
--- a/common/djangoapps/student/management/commands/tests/test_pearson.py
+++ b/common/djangoapps/student/management/commands/tests/test_pearson.py
@@ -4,14 +4,16 @@ Created on Jan 17, 2013
@author: brian
'''
import logging
+import os
+from tempfile import mkdtemp
+import cStringIO
+import sys
from django.test import TestCase
-from student.models import User, TestCenterRegistration, TestCenterUser
-# This is stupid! Because I import a function with the word "test" in the name,
-# the unittest framework tries to run *it* as a test?! Crazy!
-from student.models import get_tc_registration
from django.core import management
+from student.models import User, TestCenterRegistration, TestCenterUser, get_tc_registration
+
log = logging.getLogger(__name__)
@@ -49,7 +51,40 @@ class PearsonTestCase(TestCase):
'''
Base class for tests running Pearson-related commands
'''
+ import_dir = mkdtemp(prefix="import")
+ export_dir = mkdtemp(prefix="export")
+
+
+ def tearDown(self):
+ def delete_temp_dir(dirname):
+ if os.path.exists(dirname):
+ for filename in os.listdir(dirname):
+ os.remove(os.path.join(dirname, filename))
+ os.rmdir(dirname)
+
+ # clean up after any test data was dumped to temp directory
+ delete_temp_dir(self.import_dir)
+ delete_temp_dir(self.export_dir)
+ def test_missing_demographic_fields(self):
+ old_stdout = sys.stdout
+ sys.stdout = cStringIO.StringIO()
+ username = 'baduser'
+ User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
+ options = {}
+
+ self.assertRaises(BaseException, management.call_command, 'pearson_make_tc_user', username, **options)
+ output_string = sys.stdout.getvalue()
+ self.assertTrue(output_string.find('Field Form errors encountered:') >= 0)
+ self.assertTrue(output_string.find('Field Form Error: city') >= 0)
+ self.assertTrue(output_string.find('Field Form Error: first_name') >= 0)
+ self.assertTrue(output_string.find('Field Form Error: last_name') >= 0)
+ self.assertTrue(output_string.find('Field Form Error: country') >= 0)
+ self.assertTrue(output_string.find('Field Form Error: phone_country_code') >= 0)
+ self.assertTrue(output_string.find('Field Form Error: phone') >= 0)
+ self.assertTrue(output_string.find('Field Form Error: address_1') >= 0)
+ sys.stdout = old_stdout
+
def test_create_good_testcenter_user(self):
testcenter_user = create_tc_user("test1")
@@ -68,10 +103,15 @@ class PearsonTestCase(TestCase):
accommodation_code = 'NONE'
testcenter_user = create_tc_user(username)
registration = create_tc_registration(username, course_id, exam_code, accommodation_code)
- output_dir = "./tmpOutput"
- options = { 'destination' : output_dir }
- with self.settings(PEARSON={ 'LOCAL_EXPORT' : output_dir }):
+ #options = { 'destination' : self.export_dir }
+ options = { '--dest-from-settings' : None }
+ with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
management.call_command('pearson_export_cdd', **options)
+ print 'Files found: {}'.format(os.listdir(self.export_dir))
+ self.assertEquals(len(os.listdir(self.export_dir)), 1, "Expect cdd file to be created")
management.call_command('pearson_export_ead', **options)
+ print 'Files found: {}'.format(os.listdir(self.export_dir))
+ self.assertEquals(len(os.listdir(self.export_dir)), 2, "Expect ead file to also be created")
+
# TODO: check that files were output....
From 740d0403e908d32d656b1b8c8631db759d4b4c7b Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Tue, 22 Jan 2013 11:32:39 -0500
Subject: [PATCH 29/82] change name of function back to
get_testcenter_registration, and disable its use as a test
---
.../management/commands/pearson_make_tc_registration.py | 6 +++---
.../student/management/commands/tests/test_pearson.py | 4 ++--
common/djangoapps/student/models.py | 8 ++++++--
common/djangoapps/student/views.py | 6 +++---
4 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
index 545c8cd8a8..81a478d19d 100644
--- a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
+++ b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
@@ -4,7 +4,7 @@ from time import strftime
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
-from student.models import TestCenterUser, TestCenterRegistration, TestCenterRegistrationForm, get_tc_registration
+from student.models import TestCenterUser, TestCenterRegistration, TestCenterRegistrationForm, get_testcenter_registration
from student.views import course_from_id
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
@@ -134,7 +134,7 @@ class Command(BaseCommand):
# create and save the registration:
needs_updating = False
- registrations = get_tc_registration(student, course_id, exam_code)
+ registrations = get_testcenter_registration(student, course_id, exam_code)
if len(registrations) > 0:
registration = registrations[0]
for fieldname in UPDATE_FIELDS:
@@ -181,7 +181,7 @@ class Command(BaseCommand):
change_internal = False
if 'exam_series_code' in our_options:
exam_code = our_options['exam_series_code']
- registration = get_tc_registration(student, course_id, exam_code)[0]
+ registration = get_testcenter_registration(student, course_id, exam_code)[0]
for internal_field in [ 'upload_error_message', 'upload_status', 'authorization_id']:
if internal_field in our_options:
registration.__setattr__(internal_field, our_options[internal_field])
diff --git a/common/djangoapps/student/management/commands/tests/test_pearson.py b/common/djangoapps/student/management/commands/tests/test_pearson.py
index fd9eb31631..538cd2812a 100644
--- a/common/djangoapps/student/management/commands/tests/test_pearson.py
+++ b/common/djangoapps/student/management/commands/tests/test_pearson.py
@@ -12,7 +12,7 @@ import sys
from django.test import TestCase
from django.core import management
-from student.models import User, TestCenterRegistration, TestCenterUser, get_tc_registration
+from student.models import User, TestCenterRegistration, TestCenterUser, get_testcenter_registration
log = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ def create_tc_registration(username, course_id, exam_code, accommodation_code):
management.call_command('pearson_make_tc_registration', username, course_id, **options)
user = User.objects.get(username=username)
- registrations = get_tc_registration(user, course_id, exam_code)
+ registrations = get_testcenter_registration(user, course_id, exam_code)
return registrations[0]
class PearsonTestCase(TestCase):
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 84bbc76a80..0d8a643ecb 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -547,13 +547,17 @@ class TestCenterRegistrationForm(ModelForm):
-def get_tc_registration(user, course_id, exam_series_code):
+def get_testcenter_registration(user, course_id, exam_series_code):
try:
tcu = TestCenterUser.objects.get(user=user)
except TestCenterUser.DoesNotExist:
return []
return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id, exam_series_code=exam_series_code)
-
+
+# nosetests thinks that anything with _test_ in the name is a test.
+# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
+get_testcenter_registration.__test__ = False
+
def unique_id_for_user(user):
"""
Return a unique id for a user, suitable for inserting into
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 650f0a0280..61b49e6022 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -31,7 +31,7 @@ from student.models import (Registration, UserProfile, TestCenterUser, TestCente
TestCenterRegistration, TestCenterRegistrationForm,
PendingNameChange, PendingEmailChange,
CourseEnrollment, unique_id_for_user,
- get_tc_registration)
+ get_testcenter_registration)
from certificates.models import CertificateStatuses, certificate_status_for_student
@@ -612,7 +612,7 @@ def exam_registration_info(user, course):
return None
exam_code = exam_info.exam_series_code
- registrations = get_tc_registration(user, course.id, exam_code)
+ registrations = get_testcenter_registration(user, course.id, exam_code)
if registrations:
registration = registrations[0]
else:
@@ -712,7 +712,7 @@ def create_exam_registration(request, post_override=None):
needs_saving = False
exam = course.current_test_center_exam
exam_code = exam.exam_series_code
- registrations = get_tc_registration(user, course_id, exam_code)
+ registrations = get_testcenter_registration(user, course_id, exam_code)
if registrations:
registration = registrations[0]
# NOTE: we do not bother to check here to see if the registration has changed,
From d395c4448d09e77446ac83533c6342b4e0b738fa Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Tue, 22 Jan 2013 17:58:40 -0500
Subject: [PATCH 30/82] add more pearson tests, and update commands in response
---
.../management/commands/pearson_export_cdd.py | 9 +-
.../management/commands/pearson_export_ead.py | 8 +-
.../management/commands/pearson_transfer.py | 11 +-
.../management/commands/tests/test_pearson.py | 192 ++++++++++++++----
4 files changed, 176 insertions(+), 44 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_export_cdd.py b/common/djangoapps/student/management/commands/pearson_export_cdd.py
index ba43dd3bc0..463eec6b70 100644
--- a/common/djangoapps/student/management/commands/pearson_export_cdd.py
+++ b/common/djangoapps/student/management/commands/pearson_export_cdd.py
@@ -5,7 +5,7 @@ from datetime import datetime
from optparse import make_option
from django.conf import settings
-from django.core.management.base import BaseCommand
+from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser
@@ -39,6 +39,9 @@ class Command(BaseCommand):
("LastUpdate", "user_updated_at"), # in UTC, so same as what we store
])
+ # define defaults, even thought 'store_true' shouldn't need them.
+ # (call_command will set None as default value for all options that don't have one,
+ # so one cannot rely on presence/absence of flags in that world.)
option_list = BaseCommand.option_list + (
make_option('--dest-from-settings',
action='store_true',
@@ -63,13 +66,13 @@ class Command(BaseCommand):
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
- if 'dest-from-settings' in options:
+ if 'dest-from-settings' in options and options['dest-from-settings']:
if 'LOCAL_EXPORT' in settings.PEARSON:
dest = settings.PEARSON['LOCAL_EXPORT']
else:
raise CommandError('--dest-from-settings was enabled but the'
'PEARSON[LOCAL_EXPORT] setting was not set.')
- elif 'destination' in options:
+ elif 'destination' in options and options['destination']:
dest = options['destination']
else:
raise CommandError('--destination or --dest-from-settings must be used')
diff --git a/common/djangoapps/student/management/commands/pearson_export_ead.py b/common/djangoapps/student/management/commands/pearson_export_ead.py
index 492cba154b..49cdc9957a 100644
--- a/common/djangoapps/student/management/commands/pearson_export_ead.py
+++ b/common/djangoapps/student/management/commands/pearson_export_ead.py
@@ -5,7 +5,7 @@ from datetime import datetime
from optparse import make_option
from django.conf import settings
-from django.core.management.base import BaseCommand
+from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterRegistration
@@ -39,10 +39,12 @@ class Command(BaseCommand):
make_option('--dump_all',
action='store_true',
dest='dump_all',
+ default=False,
),
make_option('--force_add',
action='store_true',
dest='force_add',
+ default=False,
),
)
@@ -57,13 +59,13 @@ class Command(BaseCommand):
# Name will use timestamp -- this is UTC, so it will look funny,
# but it should at least be consistent with the other timestamps
# used in the system.
- if 'dest-from-settings' in options:
+ if 'dest-from-settings' in options and options['dest-from-settings']:
if 'LOCAL_EXPORT' in settings.PEARSON:
dest = settings.PEARSON['LOCAL_EXPORT']
else:
raise CommandError('--dest-from-settings was enabled but the'
'PEARSON[LOCAL_EXPORT] setting was not set.')
- elif 'destinations' in options:
+ elif 'destination' in options and options['destination']:
dest = options['destination']
else:
raise CommandError('--destination or --dest-from-settings must be used')
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index 5f126a24f0..c216d2ceac 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -1,10 +1,12 @@
+import os
from optparse import make_option
+from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
+from django.core.management import call_command
from dogapi import dog_http_api, dog_stats_api
import paramiko
import boto
-import os
dog_http_api.api_key = settings.DATADOG_API
@@ -13,7 +15,7 @@ class Command(BaseCommand):
help = """
This command handles the importing and exporting of student records for
Pearson. It uses some other Django commands to export and import the
- files and then uploads over SFTP to pearson and stuffs the entry in an
+ files and then uploads over SFTP to Pearson and stuffs the entry in an
S3 bucket for archive purposes.
Usage: django-admin.py pearson-transfer --mode [import|export|both]
@@ -29,11 +31,12 @@ class Command(BaseCommand):
def handle(self, **options):
+ # TODO: this doesn't work. Need to check if it's a property.
if not settings.PEARSON:
raise CommandError('No PEARSON entries in auth/env.json.')
- for value in ['LOCAL_IMPORT', 'SFTP_IMPORT', 'BUCKET', 'LOCAL_EXPORT',
- 'SFTP_EXPORT']:
+ for value in ['LOCAL_IMPORT', 'SFTP_IMPORT', 'LOCAL_EXPORT',
+ 'SFTP_EXPORT', 'SFTP_HOSTNAME', 'SFTP_USERNAME', 'SFTP_PASSWORD']:
if value not in settings.PEARSON:
raise CommandError('No entry in the PEARSON settings'
'(env/auth.json) for {0}'.format(value))
diff --git a/common/djangoapps/student/management/commands/tests/test_pearson.py b/common/djangoapps/student/management/commands/tests/test_pearson.py
index 538cd2812a..d5594926d2 100644
--- a/common/djangoapps/student/management/commands/tests/test_pearson.py
+++ b/common/djangoapps/student/management/commands/tests/test_pearson.py
@@ -10,7 +10,7 @@ import cStringIO
import sys
from django.test import TestCase
-from django.core import management
+from django.core.management import call_command
from student.models import User, TestCenterRegistration, TestCenterUser, get_testcenter_registration
@@ -30,11 +30,11 @@ def create_tc_user(username):
'phone' : '252-1866',
'phone_country_code' : '1',
}
- management.call_command('pearson_make_tc_user', username, **options)
+ call_command('pearson_make_tc_user', username, **options)
return TestCenterUser.objects.get(user=user)
-def create_tc_registration(username, course_id, exam_code, accommodation_code):
+def create_tc_registration(username, course_id = 'org1/course1/term1', exam_code = 'exam1', accommodation_code = None):
options = { 'exam_series_code' : exam_code,
'eligibility_appointment_date_first' : '2013-01-01T00:00',
@@ -42,11 +42,55 @@ def create_tc_registration(username, course_id, exam_code, accommodation_code):
'accommodation_code' : accommodation_code,
}
- management.call_command('pearson_make_tc_registration', username, course_id, **options)
+ call_command('pearson_make_tc_registration', username, course_id, **options)
user = User.objects.get(username=username)
registrations = get_testcenter_registration(user, course_id, exam_code)
return registrations[0]
+
+def get_error_string_for_management_call(*args, **options):
+ stdout_string = None
+ old_stdout = sys.stdout
+ old_stderr = sys.stderr
+ sys.stdout = cStringIO.StringIO()
+ sys.stderr = cStringIO.StringIO()
+ try:
+ call_command(*args, **options)
+ except BaseException, why1:
+ # The goal here is to catch CommandError calls.
+ # But these are actually translated into nice messages,
+ # and sys.exit(1) is then called. For testing, we
+ # want to catch what sys.exit throws, and get the
+ # relevant text either from stdout or stderr.
+ # TODO: this should really check to see that we
+ # arrived here because of a sys.exit(1). Otherwise
+ # we should just raise the exception.
+ stdout_string = sys.stdout.getvalue()
+ stderr_string = sys.stderr.getvalue()
+ except Exception, why:
+ raise why
+ finally:
+ sys.stdout = old_stdout
+ sys.stderr = old_stderr
+
+ if stdout_string is None:
+ raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
+ return stdout_string, stderr_string
+
+
+def get_file_info(dirpath):
+ filelist = os.listdir(dirpath)
+ print 'Files found: {}'.format(filelist)
+ numfiles = len(filelist)
+ if numfiles == 1:
+ filepath = os.path.join(dirpath, filelist[0])
+ with open(filepath, 'r') as cddfile:
+ filecontents = cddfile.readlines()
+ numlines = len(filecontents)
+ return filepath, numlines
+ else:
+ raise Exception("Expected to find a single file in {}, but found {}".format(dirpath,filelist))
+
class PearsonTestCase(TestCase):
'''
Base class for tests running Pearson-related commands
@@ -54,7 +98,9 @@ class PearsonTestCase(TestCase):
import_dir = mkdtemp(prefix="import")
export_dir = mkdtemp(prefix="export")
-
+ def assertErrorContains(self, error_message, expected):
+ self.assertTrue(error_message.find(expected) >= 0, 'error message "{}" did not contain "{}"'.format(error_message, expected))
+
def tearDown(self):
def delete_temp_dir(dirname):
if os.path.exists(dirname):
@@ -67,14 +113,13 @@ class PearsonTestCase(TestCase):
delete_temp_dir(self.export_dir)
def test_missing_demographic_fields(self):
- old_stdout = sys.stdout
- sys.stdout = cStringIO.StringIO()
+ # We won't bother to test all details of form validation here.
+ # It is enough to show that it works here, but deal with test cases for the form
+ # validation in the student tests, not these management tests.
username = 'baduser'
User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
options = {}
-
- self.assertRaises(BaseException, management.call_command, 'pearson_make_tc_user', username, **options)
- output_string = sys.stdout.getvalue()
+ output_string, _ = get_error_string_for_management_call('pearson_make_tc_user', username, **options)
self.assertTrue(output_string.find('Field Form errors encountered:') >= 0)
self.assertTrue(output_string.find('Field Form Error: city') >= 0)
self.assertTrue(output_string.find('Field Form Error: first_name') >= 0)
@@ -83,35 +128,114 @@ class PearsonTestCase(TestCase):
self.assertTrue(output_string.find('Field Form Error: phone_country_code') >= 0)
self.assertTrue(output_string.find('Field Form Error: phone') >= 0)
self.assertTrue(output_string.find('Field Form Error: address_1') >= 0)
- sys.stdout = old_stdout
+ self.assertErrorContains(output_string, 'Field Form Error: address_1')
def test_create_good_testcenter_user(self):
testcenter_user = create_tc_user("test1")
+ self.assertIsNotNone(testcenter_user)
def test_create_good_testcenter_registration(self):
username = 'test1'
- course_id = 'org1/course1/term1'
- exam_code = 'exam1'
- accommodation_code = 'NONE'
- testcenter_user = create_tc_user(username)
- registration = create_tc_registration(username, course_id, exam_code, accommodation_code)
-
- def test_export(self):
- username = 'test1'
- course_id = 'org1/course1/term1'
- exam_code = 'exam1'
- accommodation_code = 'NONE'
- testcenter_user = create_tc_user(username)
- registration = create_tc_registration(username, course_id, exam_code, accommodation_code)
- #options = { 'destination' : self.export_dir }
- options = { '--dest-from-settings' : None }
- with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
- management.call_command('pearson_export_cdd', **options)
- print 'Files found: {}'.format(os.listdir(self.export_dir))
- self.assertEquals(len(os.listdir(self.export_dir)), 1, "Expect cdd file to be created")
- management.call_command('pearson_export_ead', **options)
- print 'Files found: {}'.format(os.listdir(self.export_dir))
- self.assertEquals(len(os.listdir(self.export_dir)), 2, "Expect ead file to also be created")
+ create_tc_user(username)
+ registration = create_tc_registration(username)
+ self.assertIsNotNone(registration)
- # TODO: check that files were output....
+ def test_cdd_missing_option(self):
+ _, error_string = get_error_string_for_management_call('pearson_export_cdd', **{})
+ self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
+
+ def test_ead_missing_option(self):
+ _, error_string = get_error_string_for_management_call('pearson_export_ead', **{})
+ self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
+
+ def test_export_single_cdd(self):
+ # before we generate any tc_users, we expect there to be nothing to output:
+ options = { 'dest-from-settings' : True }
+ with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
+ call_command('pearson_export_cdd', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 1, "Expect cdd file to have no non-header lines")
+ os.remove(filepath)
+
+ # generating a tc_user should result in a line in the output
+ username = 'test_single_cdd'
+ create_tc_user(username)
+ call_command('pearson_export_cdd', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 2, "Expect cdd file to have one non-header line")
+ os.remove(filepath)
+
+ # output after registration should not have any entries again.
+ call_command('pearson_export_cdd', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 1, "Expect cdd file to have no non-header lines")
+ os.remove(filepath)
+
+ # if we modify the record, then it should be output again:
+ user_options = { 'first_name' : 'NewTestFirst', }
+ call_command('pearson_make_tc_user', username, **user_options)
+ call_command('pearson_export_cdd', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 2, "Expect cdd file to have one non-header line")
+ os.remove(filepath)
+
+ def test_export_single_ead(self):
+ # before we generate any registrations, we expect there to be nothing to output:
+ options = { 'dest-from-settings' : True }
+ with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
+ call_command('pearson_export_ead', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
+ os.remove(filepath)
+
+ # generating a registration should result in a line in the output
+ username = 'test_single_ead'
+ create_tc_user(username)
+ create_tc_registration(username)
+ call_command('pearson_export_ead', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 2, "Expect ead file to have one non-header line")
+ os.remove(filepath)
+
+ # output after registration should not have any entries again.
+ call_command('pearson_export_ead', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 1, "Expect ead file to have no non-header lines")
+ os.remove(filepath)
+ # if we modify the record, then it should be output again:
+ create_tc_registration(username, accommodation_code='EQPMNT')
+ call_command('pearson_export_ead', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 2, "Expect ead file to have one non-header line")
+ os.remove(filepath)
+
+ def test_export_multiple(self):
+ username1 = 'test_multiple1'
+ create_tc_user(username1)
+ create_tc_registration(username1)
+ create_tc_registration(username1, course_id = 'org1/course2/term1')
+ create_tc_registration(username1, exam_code = 'exam2')
+ username2 = 'test_multiple2'
+ create_tc_user(username2)
+ create_tc_registration(username2)
+ username3 = 'test_multiple3'
+ create_tc_user(username3)
+ create_tc_registration(username3, course_id = 'org1/course2/term1')
+ username4 = 'test_multiple4'
+ create_tc_user(username4)
+ create_tc_registration(username4, exam_code = 'exam2')
+
+ with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
+ options = { 'dest-from-settings' : True }
+ call_command('pearson_export_cdd', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 5, "Expect cdd file to have four non-header lines: total was {}".format(numlines))
+ os.remove(filepath)
+
+ call_command('pearson_export_ead', **options)
+ (filepath, numlines) = get_file_info(self.export_dir)
+ self.assertEquals(numlines, 7, "Expect ead file to have six non-header lines: total was {}".format(numlines))
+ os.remove(filepath)
+
+
From 1199b1ecfa97cf623d1275ea0220af99119ba8e3 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Wed, 23 Jan 2013 18:22:18 -0500
Subject: [PATCH 31/82] debug pearson import and update unit tests
---
.../commands/pearson_import_conf_zip.py | 2 +
.../commands/pearson_make_tc_user.py | 19 +-
.../management/commands/pearson_transfer.py | 133 +++++++----
.../management/commands/tests/test_pearson.py | 209 +++++++++++++++---
4 files changed, 275 insertions(+), 88 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
index fa9741dc68..bf7c4481fd 100644
--- a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
+++ b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
@@ -10,6 +10,7 @@ from optparse import make_option
from dogapi import dog_http_api, dog_stats_api
from django.core.management.base import BaseCommand, CommandError
+from django.conf import settings
from student.models import TestCenterUser, TestCenterRegistration
@@ -23,6 +24,7 @@ class Command(BaseCommand):
and TestCenterRegistration tables with status.
"""
+ @staticmethod
def datadog_error(string, tags):
dog_http_api.event("Pearson Import", string, alert_type='error', tags=tags)
diff --git a/common/djangoapps/student/management/commands/pearson_make_tc_user.py b/common/djangoapps/student/management/commands/pearson_make_tc_user.py
index da9bfc3bd0..87e0b4dadd 100644
--- a/common/djangoapps/student/management/commands/pearson_make_tc_user.py
+++ b/common/djangoapps/student/management/commands/pearson_make_tc_user.py
@@ -1,7 +1,7 @@
from optparse import make_option
from django.contrib.auth.models import User
-from django.core.management.base import BaseCommand
+from django.core.management.base import BaseCommand, CommandError
from student.models import TestCenterUser, TestCenterUserForm
@@ -161,15 +161,16 @@ class Command(BaseCommand):
if form.is_valid():
form.update_and_save()
else:
+ errorlist = []
if (len(form.errors) > 0):
- print "Field Form errors encountered:"
- for fielderror in form.errors:
- print "Field Form Error: %s" % fielderror
- if (len(form.non_field_errors()) > 0):
- print "Non-field Form errors encountered:"
- for nonfielderror in form.non_field_errors:
- print "Non-field Form Error: %s" % nonfielderror
-
+ errorlist.append("Field Form errors encountered:")
+ for fielderror in form.errors:
+ errorlist.append("Field Form Error: {}".format(fielderror))
+ if (len(form.non_field_errors()) > 0):
+ errorlist.append("Non-field Form errors encountered:")
+ for nonfielderror in form.non_field_errors:
+ errorlist.append("Non-field Form Error: {}".format(nonfielderror))
+ raise CommandError("\n".join(errorlist))
else:
print "No changes necessary to make to existing user's demographics."
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index c216d2ceac..2124bdceb6 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -1,5 +1,6 @@
import os
from optparse import make_option
+from stat import S_ISDIR
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
@@ -26,85 +27,99 @@ class Command(BaseCommand):
action='store',
dest='mode',
default='both',
+ choices=('import', 'export', 'both'),
help='mode is import, export, or both'),
)
def handle(self, **options):
- # TODO: this doesn't work. Need to check if it's a property.
- if not settings.PEARSON:
+ if not hasattr(settings, 'PEARSON'):
raise CommandError('No PEARSON entries in auth/env.json.')
- for value in ['LOCAL_IMPORT', 'SFTP_IMPORT', 'LOCAL_EXPORT',
- 'SFTP_EXPORT', 'SFTP_HOSTNAME', 'SFTP_USERNAME', 'SFTP_PASSWORD']:
+ # check settings needed for either import or export:
+ for value in ['SFTP_HOSTNAME', 'SFTP_USERNAME', 'SFTP_PASSWORD', 'S3_BUCKET']:
if value not in settings.PEARSON:
raise CommandError('No entry in the PEARSON settings'
'(env/auth.json) for {0}'.format(value))
- def import_pearson():
- try:
- sftp(settings.PEARSON['SFTP_IMPORT'],
- settings.PEARSON['LOCAL_IMPORT'], options['mode'])
- s3(settings.PEARSON['LOCAL_IMPORT'],
- settings.PEARSON['BUCKET'], options['mode'])
- except Exception as e:
- dog_http_api.event('Pearson Import failure', str(e))
- else:
- for file in os.listdir(settings.PEARSON['LOCAL_IMPORT']):
- call_command('pearson_import_conf_zip',
- settings.PEARSON['LOCAL_IMPORT'] + '/' + file)
- os.remove(file)
+ for value in ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY']:
+ if not hasattr(settings, value):
+ raise CommandError('No entry in the AWS settings'
+ '(env/auth.json) for {0}'.format(value))
+
+ # check additional required settings for import and export:
+ if options['mode'] in ('export', 'both'):
+ for value in ['LOCAL_EXPORT','SFTP_EXPORT']:
+ if value not in settings.PEARSON:
+ raise CommandError('No entry in the PEARSON settings'
+ '(env/auth.json) for {0}'.format(value))
+ # make sure that the import directory exists or can be created:
+ source_dir = settings.PEARSON['LOCAL_EXPORT']
+ if not os.path.isdir(source_dir):
+ os.makedirs(source_dir)
+
+ if options['mode'] in ('import', 'both'):
+ for value in ['LOCAL_IMPORT','SFTP_IMPORT']:
+ if value not in settings.PEARSON:
+ raise CommandError('No entry in the PEARSON settings'
+ '(env/auth.json) for {0}'.format(value))
+ # make sure that the import directory exists or can be created:
+ dest_dir = settings.PEARSON['LOCAL_IMPORT']
+ if not os.path.isdir(dest_dir):
+ os.makedirs(dest_dir)
- def export_pearson():
- call_command('pearson_export_cdd', 'dest_from_settings')
- call_command('pearson_export_ead', 'dest_from_settings')
- sftp(settings.PEARSON['LOCAL_EXPORT'],
- settings.PEARSON['SFTP_EXPORT'], options['mode'])
- s3(settings.PEARSON['LOCAL_EXPORT'],
- settings.PEARSON['BUCKET'], options['mode'])
- if options['mode'] == 'export':
- export_pearson()
- elif options['mode'] == 'import':
- import_pearson()
- else:
- export_pearson()
- import_pearson()
-
- def sftp(files_from, files_to, mode):
+ def sftp(files_from, files_to, mode, deleteAfterCopy=False):
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='sftp'):
try:
t = paramiko.Transport((settings.PEARSON['SFTP_HOSTNAME'], 22))
t.connect(username=settings.PEARSON['SFTP_USERNAME'],
password=settings.PEARSON['SFTP_PASSWORD'])
sftp = paramiko.SFTPClient.from_transport(t)
- if os.path.isdir(files_from):
+
+ if mode == 'export':
+ try:
+ sftp.chdir(files_to)
+ except IOError:
+ raise CommandError('SFTP destination path does not exist: {}'.format(files_to))
for filename in os.listdir(files_from):
- sftp.put(files_from + '/' + filename,
- files_to + '/' + filename)
+ sftp.put(files_from + '/' + filename, filename)
+ if deleteAfterCopy:
+ os.remove(os.path.join(files_from, filename))
else:
- for filename in sftp.listdir(files_from):
- sftp.get(files_from + '/' + filename,
- files_to + '/' + filename)
- sftp.remove(files_from + '/' + filename)
- t.close()
+ try:
+ sftp.chdir(files_from)
+ except IOError:
+ raise CommandError('SFTP source path does not exist: {}'.format(files_from))
+ for filename in sftp.listdir('.'):
+ # skip subdirectories
+ if not S_ISDIR(sftp.stat(filename).st_mode):
+ sftp.get(filename, files_to + '/' + filename)
+ # delete files from sftp server once they are successfully pulled off:
+ if deleteAfterCopy:
+ sftp.remove(filename)
except:
dog_http_api.event('pearson {0}'.format(mode),
'sftp uploading failed',
alert_type='error')
raise
+ finally:
+ sftp.close()
+ t.close()
- def s3(files_from, bucket, mode):
+ def s3(files_from, bucket, mode, deleteAfterCopy=False):
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='s3'):
try:
for filename in os.listdir(files_from):
- upload_file_to_s3(bucket, files_from + '/' + filename)
+ upload_file_to_s3(bucket, files_from, filename)
+ if deleteAfterCopy:
+ os.remove(files_from + '/' + filename)
except:
dog_http_api.event('pearson {0}'.format(mode),
's3 archiving failed')
raise
- def upload_file_to_s3(bucket, filename):
+ def upload_file_to_s3(bucket, source_dir, filename):
"""
Upload file to S3
"""
@@ -114,4 +129,32 @@ class Command(BaseCommand):
b = s3.get_bucket(bucket)
k = Key(b)
k.key = "{filename}".format(filename=filename)
- k.set_contents_from_filename(filename)
+ k.set_contents_from_filename(os.path.join(source_dir, filename))
+
+ def export_pearson():
+ options = { 'dest-from-settings' : True }
+ call_command('pearson_export_cdd', **options)
+ call_command('pearson_export_ead', **options)
+ mode = 'export'
+ sftp(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['SFTP_EXPORT'], mode, deleteAfterCopy = False)
+ s3(settings.PEARSON['LOCAL_EXPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=True)
+
+ def import_pearson():
+ mode = 'import'
+ try:
+ sftp(settings.PEARSON['SFTP_IMPORT'], settings.PEARSON['LOCAL_IMPORT'], mode, deleteAfterCopy = True)
+ s3(settings.PEARSON['LOCAL_IMPORT'], settings.PEARSON['S3_BUCKET'], mode, deleteAfterCopy=False)
+ except Exception as e:
+ dog_http_api.event('Pearson Import failure', str(e))
+ raise e
+ else:
+ for filename in os.listdir(settings.PEARSON['LOCAL_IMPORT']):
+ filepath = os.path.join(settings.PEARSON['LOCAL_IMPORT'], filename)
+ call_command('pearson_import_conf_zip', filepath)
+ os.remove(filepath)
+
+ # actually do the work!
+ if options['mode'] in ('export', 'both'):
+ export_pearson()
+ if options['mode'] in ('import', 'both'):
+ import_pearson()
diff --git a/common/djangoapps/student/management/commands/tests/test_pearson.py b/common/djangoapps/student/management/commands/tests/test_pearson.py
index d5594926d2..199557bf87 100644
--- a/common/djangoapps/student/management/commands/tests/test_pearson.py
+++ b/common/djangoapps/student/management/commands/tests/test_pearson.py
@@ -11,12 +11,12 @@ import sys
from django.test import TestCase
from django.core.management import call_command
+from nose.plugins.skip import SkipTest
from student.models import User, TestCenterRegistration, TestCenterUser, get_testcenter_registration
log = logging.getLogger(__name__)
-
def create_tc_user(username):
user = User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
options = {
@@ -47,6 +47,48 @@ def create_tc_registration(username, course_id = 'org1/course1/term1', exam_code
registrations = get_testcenter_registration(user, course_id, exam_code)
return registrations[0]
+def create_multiple_registrations(prefix='test'):
+ username1 = '{}_multiple1'.format(prefix)
+ create_tc_user(username1)
+ create_tc_registration(username1)
+ create_tc_registration(username1, course_id = 'org1/course2/term1')
+ create_tc_registration(username1, exam_code = 'exam2')
+ username2 = '{}_multiple2'.format(prefix)
+ create_tc_user(username2)
+ create_tc_registration(username2)
+ username3 = '{}_multiple3'.format(prefix)
+ create_tc_user(username3)
+ create_tc_registration(username3, course_id = 'org1/course2/term1')
+ username4 = '{}_multiple4'.format(prefix)
+ create_tc_user(username4)
+ create_tc_registration(username4, exam_code = 'exam2')
+
+def get_command_error_text(*args, **options):
+ stderr_string = None
+ old_stderr = sys.stderr
+ sys.stderr = cStringIO.StringIO()
+ try:
+ call_command(*args, **options)
+ except SystemExit, why1:
+ # The goal here is to catch CommandError calls.
+ # But these are actually translated into nice messages,
+ # and sys.exit(1) is then called. For testing, we
+ # want to catch what sys.exit throws, and get the
+ # relevant text either from stdout or stderr.
+ if (why1.message > 0):
+ stderr_string = sys.stderr.getvalue()
+ else:
+ raise why1
+ except Exception, why:
+ raise why
+
+ finally:
+ sys.stderr = old_stderr
+
+ if stderr_string is None:
+ raise Exception("Expected call to {} to fail, but it succeeded!".format(args[0]))
+ return stderr_string
+
def get_error_string_for_management_call(*args, **options):
stdout_string = None
old_stdout = sys.stdout
@@ -55,17 +97,17 @@ def get_error_string_for_management_call(*args, **options):
sys.stderr = cStringIO.StringIO()
try:
call_command(*args, **options)
- except BaseException, why1:
+ except SystemExit, why1:
# The goal here is to catch CommandError calls.
# But these are actually translated into nice messages,
# and sys.exit(1) is then called. For testing, we
# want to catch what sys.exit throws, and get the
# relevant text either from stdout or stderr.
- # TODO: this should really check to see that we
- # arrived here because of a sys.exit(1). Otherwise
- # we should just raise the exception.
- stdout_string = sys.stdout.getvalue()
- stderr_string = sys.stderr.getvalue()
+ if (why1.message == 1):
+ stdout_string = sys.stdout.getvalue()
+ stderr_string = sys.stderr.getvalue()
+ else:
+ raise why1
except Exception, why:
raise why
@@ -111,6 +153,12 @@ class PearsonTestCase(TestCase):
# clean up after any test data was dumped to temp directory
delete_temp_dir(self.import_dir)
delete_temp_dir(self.export_dir)
+
+ # and clean up the database:
+# TestCenterUser.objects.all().delete()
+# TestCenterRegistration.objects.all().delete()
+
+class PearsonCommandTestCase(PearsonTestCase):
def test_missing_demographic_fields(self):
# We won't bother to test all details of form validation here.
@@ -119,16 +167,16 @@ class PearsonTestCase(TestCase):
username = 'baduser'
User.objects.create_user(username, '{}@edx.org'.format(username), 'fakepass')
options = {}
- output_string, _ = get_error_string_for_management_call('pearson_make_tc_user', username, **options)
- self.assertTrue(output_string.find('Field Form errors encountered:') >= 0)
- self.assertTrue(output_string.find('Field Form Error: city') >= 0)
- self.assertTrue(output_string.find('Field Form Error: first_name') >= 0)
- self.assertTrue(output_string.find('Field Form Error: last_name') >= 0)
- self.assertTrue(output_string.find('Field Form Error: country') >= 0)
- self.assertTrue(output_string.find('Field Form Error: phone_country_code') >= 0)
- self.assertTrue(output_string.find('Field Form Error: phone') >= 0)
- self.assertTrue(output_string.find('Field Form Error: address_1') >= 0)
- self.assertErrorContains(output_string, 'Field Form Error: address_1')
+ error_string = get_command_error_text('pearson_make_tc_user', username, **options)
+ self.assertTrue(error_string.find('Field Form errors encountered:') >= 0)
+ self.assertTrue(error_string.find('Field Form Error: city') >= 0)
+ self.assertTrue(error_string.find('Field Form Error: first_name') >= 0)
+ self.assertTrue(error_string.find('Field Form Error: last_name') >= 0)
+ self.assertTrue(error_string.find('Field Form Error: country') >= 0)
+ self.assertTrue(error_string.find('Field Form Error: phone_country_code') >= 0)
+ self.assertTrue(error_string.find('Field Form Error: phone') >= 0)
+ self.assertTrue(error_string.find('Field Form Error: address_1') >= 0)
+ self.assertErrorContains(error_string, 'Field Form Error: address_1')
def test_create_good_testcenter_user(self):
testcenter_user = create_tc_user("test1")
@@ -141,11 +189,11 @@ class PearsonTestCase(TestCase):
self.assertIsNotNone(registration)
def test_cdd_missing_option(self):
- _, error_string = get_error_string_for_management_call('pearson_export_cdd', **{})
+ error_string = get_command_error_text('pearson_export_cdd', **{})
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
def test_ead_missing_option(self):
- _, error_string = get_error_string_for_management_call('pearson_export_ead', **{})
+ error_string = get_command_error_text('pearson_export_ead', **{})
self.assertErrorContains(error_string, 'Error: --destination or --dest-from-settings must be used')
def test_export_single_cdd(self):
@@ -211,21 +259,7 @@ class PearsonTestCase(TestCase):
os.remove(filepath)
def test_export_multiple(self):
- username1 = 'test_multiple1'
- create_tc_user(username1)
- create_tc_registration(username1)
- create_tc_registration(username1, course_id = 'org1/course2/term1')
- create_tc_registration(username1, exam_code = 'exam2')
- username2 = 'test_multiple2'
- create_tc_user(username2)
- create_tc_registration(username2)
- username3 = 'test_multiple3'
- create_tc_user(username3)
- create_tc_registration(username3, course_id = 'org1/course2/term1')
- username4 = 'test_multiple4'
- create_tc_user(username4)
- create_tc_registration(username4, exam_code = 'exam2')
-
+ create_multiple_registrations("export")
with self.settings(PEARSON={ 'LOCAL_EXPORT' : self.export_dir }):
options = { 'dest-from-settings' : True }
call_command('pearson_export_cdd', **options)
@@ -239,3 +273,110 @@ class PearsonTestCase(TestCase):
os.remove(filepath)
+# def test_bad_demographic_option(self):
+# username = 'nonuser'
+# output_string, stderrmsg = get_error_string_for_management_call('pearson_make_tc_user', username, **{'--garbage' : None })
+# print stderrmsg
+# self.assertErrorContains(stderrmsg, 'Unexpected option')
+#
+# def test_missing_demographic_user(self):
+# username = 'nonuser'
+# output_string, error_string = get_error_string_for_management_call('pearson_make_tc_user', username, **{})
+# self.assertErrorContains(error_string, 'User matching query does not exist')
+
+# credentials for a test SFTP site:
+SFTP_HOSTNAME = 'ec2-23-20-150-101.compute-1.amazonaws.com'
+SFTP_USERNAME = 'pearsontest'
+SFTP_PASSWORD = 'password goes here'
+
+S3_BUCKET = 'edx-pearson-archive'
+AWS_ACCESS_KEY_ID = 'put yours here'
+AWS_SECRET_ACCESS_KEY = 'put yours here'
+
+class PearsonTransferTestCase(PearsonTestCase):
+ '''
+ Class for tests running Pearson transfers
+ '''
+
+ def test_transfer_config(self):
+ with self.settings(DATADOG_API='FAKE_KEY'):
+ # TODO: why is this failing with the wrong error message?!
+ stderrmsg = get_command_error_text('pearson_transfer', **{'mode' : 'garbage'})
+ self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
+ with self.settings(DATADOG_API='FAKE_KEY'):
+ stderrmsg = get_command_error_text('pearson_transfer')
+ self.assertErrorContains(stderrmsg, 'Error: No PEARSON entries')
+ with self.settings(DATADOG_API='FAKE_KEY',
+ PEARSON={'LOCAL_EXPORT' : self.export_dir,
+ 'LOCAL_IMPORT' : self.import_dir }):
+ stderrmsg = get_command_error_text('pearson_transfer')
+ self.assertErrorContains(stderrmsg, 'Error: No entry in the PEARSON settings')
+
+ def test_transfer_export_missing_dest_dir(self):
+ raise SkipTest()
+ create_multiple_registrations('export_missing_dest')
+ with self.settings(DATADOG_API='FAKE_KEY',
+ PEARSON={'LOCAL_EXPORT' : self.export_dir,
+ 'SFTP_EXPORT' : 'this/does/not/exist',
+ 'SFTP_HOSTNAME' : SFTP_HOSTNAME,
+ 'SFTP_USERNAME' : SFTP_USERNAME,
+ 'SFTP_PASSWORD' : SFTP_PASSWORD,
+ 'S3_BUCKET' : S3_BUCKET,
+ },
+ AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
+ AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
+ options = { 'mode' : 'export'}
+ stderrmsg = get_command_error_text('pearson_transfer', **options)
+ self.assertErrorContains(stderrmsg, 'Error: SFTP destination path does not exist')
+
+ def test_transfer_export(self):
+ raise SkipTest()
+ create_multiple_registrations("transfer_export")
+ with self.settings(DATADOG_API='FAKE_KEY',
+ PEARSON={'LOCAL_EXPORT' : self.export_dir,
+ 'SFTP_EXPORT' : 'results/topvue',
+ 'SFTP_HOSTNAME' : SFTP_HOSTNAME,
+ 'SFTP_USERNAME' : SFTP_USERNAME,
+ 'SFTP_PASSWORD' : SFTP_PASSWORD,
+ 'S3_BUCKET' : S3_BUCKET,
+ },
+ AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
+ AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
+ options = { 'mode' : 'export'}
+# call_command('pearson_transfer', **options)
+# # confirm that the export directory is still empty:
+# self.assertEqual(len(os.listdir(self.export_dir)), 0, "expected export directory to be empty")
+
+ def test_transfer_import_missing_source_dir(self):
+ raise SkipTest()
+ create_multiple_registrations('import_missing_src')
+ with self.settings(DATADOG_API='FAKE_KEY',
+ PEARSON={'LOCAL_IMPORT' : self.import_dir,
+ 'SFTP_IMPORT' : 'this/does/not/exist',
+ 'SFTP_HOSTNAME' : SFTP_HOSTNAME,
+ 'SFTP_USERNAME' : SFTP_USERNAME,
+ 'SFTP_PASSWORD' : SFTP_PASSWORD,
+ 'S3_BUCKET' : S3_BUCKET,
+ },
+ AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
+ AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
+ options = { 'mode' : 'import'}
+ stderrmsg = get_command_error_text('pearson_transfer', **options)
+ self.assertErrorContains(stderrmsg, 'Error: SFTP source path does not exist')
+
+ def test_transfer_import(self):
+ raise SkipTest()
+ create_multiple_registrations('import_missing_src')
+ with self.settings(DATADOG_API='FAKE_KEY',
+ PEARSON={'LOCAL_IMPORT' : self.import_dir,
+ 'SFTP_IMPORT' : 'results',
+ 'SFTP_HOSTNAME' : SFTP_HOSTNAME,
+ 'SFTP_USERNAME' : SFTP_USERNAME,
+ 'SFTP_PASSWORD' : SFTP_PASSWORD,
+ 'S3_BUCKET' : S3_BUCKET,
+ },
+ AWS_ACCESS_KEY_ID = AWS_ACCESS_KEY_ID,
+ AWS_SECRET_ACCESS_KEY = AWS_SECRET_ACCESS_KEY):
+ options = { 'mode' : 'import'}
+ call_command('pearson_transfer', **options)
+ self.assertEqual(len(os.listdir(self.import_dir)), 0, "expected import directory to be empty")
From 645cfbce21fef12f5793764f5ff9b6bc5111a920 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 24 Jan 2013 10:00:13 -0500
Subject: [PATCH 32/82] Move this into the github requirements where it
belongs.
---
github-requirements.txt | 1 +
requirements.txt | 1 -
2 files changed, 1 insertion(+), 1 deletion(-)
diff --git a/github-requirements.txt b/github-requirements.txt
index 468d55ce65..32193ec853 100644
--- a/github-requirements.txt
+++ b/github-requirements.txt
@@ -3,3 +3,4 @@
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
+-e git+ssh://git@github.com/MITx/dogapi.git@003a4fc9#egg=dogapi
diff --git a/requirements.txt b/requirements.txt
index 1b1384912b..fa4688b711 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -59,4 +59,3 @@ Shapely==1.2.16
ipython==0.13.1
xmltodict==0.4.1
paramiko==1.9.0
-git+ssh://git@github.com/MITx/dogapi.git@003a4fc9#egg=dogapi
From 126ff55cd530d15cd7c7b07fcf62029512da8767 Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 24 Jan 2013 10:06:09 -0500
Subject: [PATCH 33/82] Not sure why this was set to use SSH.
---
github-requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/github-requirements.txt b/github-requirements.txt
index 32193ec853..62e47a328f 100644
--- a/github-requirements.txt
+++ b/github-requirements.txt
@@ -3,4 +3,4 @@
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/MITx/django-wiki.git@e2e84558#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
--e git+ssh://git@github.com/MITx/dogapi.git@003a4fc9#egg=dogapi
+-e git://github.com/MITx/dogapi.git@003a4fc9#egg=dogapi
From cb2d8db57c60952fbec3d4eb6c9359ea437b76e2 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Thu, 24 Jan 2013 12:46:00 -0500
Subject: [PATCH 34/82] store files in S3 bucket in separate directories by
mode
---
.../student/management/commands/pearson_transfer.py | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_transfer.py b/common/djangoapps/student/management/commands/pearson_transfer.py
index 2124bdceb6..6811e1833d 100644
--- a/common/djangoapps/student/management/commands/pearson_transfer.py
+++ b/common/djangoapps/student/management/commands/pearson_transfer.py
@@ -111,7 +111,10 @@ class Command(BaseCommand):
with dog_stats_api.timer('pearson.{0}'.format(mode), tags='s3'):
try:
for filename in os.listdir(files_from):
- upload_file_to_s3(bucket, files_from, filename)
+ source_file = os.path.join(files_from, filename)
+ # use mode as name of directory into which to write files
+ dest_file = os.path.join(mode, filename)
+ upload_file_to_s3(bucket, source_file, dest_file)
if deleteAfterCopy:
os.remove(files_from + '/' + filename)
except:
@@ -119,7 +122,7 @@ class Command(BaseCommand):
's3 archiving failed')
raise
- def upload_file_to_s3(bucket, source_dir, filename):
+ def upload_file_to_s3(bucket, source_file, dest_file):
"""
Upload file to S3
"""
@@ -128,8 +131,8 @@ class Command(BaseCommand):
from boto.s3.key import Key
b = s3.get_bucket(bucket)
k = Key(b)
- k.key = "{filename}".format(filename=filename)
- k.set_contents_from_filename(os.path.join(source_dir, filename))
+ k.key = "{filename}".format(filename=dest_file)
+ k.set_contents_from_filename(source_file)
def export_pearson():
options = { 'dest-from-settings' : True }
From def2d8adf22eea0e941deb615b6543e1c531ab5d Mon Sep 17 00:00:00 2001
From: Kevin Chugh
Date: Thu, 24 Jan 2013 14:20:18 -0500
Subject: [PATCH 35/82] fix cohorts in create, and read, and update regular
expressions to fix courses with periods not working in General commentable
---
lms/djangoapps/courseware/courses.py | 10 +----
lms/djangoapps/courseware/views.py | 1 +
.../django_comment_client/base/urls.py | 8 ++--
.../django_comment_client/base/views.py | 40 ++++++++++++-------
.../django_comment_client/forum/urls.py | 4 +-
.../django_comment_client/forum/views.py | 3 +-
.../commands/seed_permissions_roles.py | 2 +-
7 files changed, 37 insertions(+), 31 deletions(-)
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index b4e8da2633..74f5e4c54f 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -84,21 +84,13 @@ def get_opt_course_with_access(user, course_id, action):
return get_course_with_access(user, course_id, action)
-
-
-def is_course_cohorted(course_id):
- """
- given a course id, return a boolean for whether or not the course is cohorted
-
- """
-
def get_cohort_id(user, course_id):
"""
given a course id and a user, return the id of the cohort that user is assigned to
and if the course is not cohorted or the user is an instructor, return None
"""
- return 101
+ return 127
def is_commentable_cohorted(course_id,commentable_id):
"""
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index b1c4a5e9a9..f170f3fb86 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -39,6 +39,7 @@ log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib}
+
def user_groups(user):
"""
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
diff --git a/lms/djangoapps/django_comment_client/base/urls.py b/lms/djangoapps/django_comment_client/base/urls.py
index f2cb4ccb15..23f2afa037 100644
--- a/lms/djangoapps/django_comment_client/base/urls.py
+++ b/lms/djangoapps/django_comment_client/base/urls.py
@@ -24,9 +24,9 @@ urlpatterns = patterns('django_comment_client.base.views',
url(r'comments/(?P[\w\-]+)/downvote$', 'vote_for_comment', {'value': 'down'}, name='downvote_comment'),
url(r'comments/(?P[\w\-]+)/unvote$', 'undo_vote_for_comment', name='undo_vote_for_comment'),
- url(r'(?P[\w\-]+)/threads/create$', 'create_thread', name='create_thread'),
+ url(r'^(?P[\w\-.]+)/threads/create$', 'create_thread', name='create_thread'),
# TODO should we search within the board?
- url(r'(?P[\w\-]+)/threads/search_similar$', 'search_similar_threads', name='search_similar_threads'),
- url(r'(?P[\w\-]+)/follow$', 'follow_commentable', name='follow_commentable'),
- url(r'(?P[\w\-]+)/unfollow$', 'unfollow_commentable', name='unfollow_commentable'),
+ url(r'^(?P[\w\-.]+)/threads/search_similar$', 'search_similar_threads', name='search_similar_threads'),
+ url(r'^(?P[\w\-.]+)/follow$', 'follow_commentable', name='follow_commentable'),
+ url(r'^(?P[\w\-.]+)/unfollow$', 'unfollow_commentable', name='unfollow_commentable'),
)
diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py
index d1948c3fc7..c1e188ff1a 100644
--- a/lms/djangoapps/django_comment_client/base/views.py
+++ b/lms/djangoapps/django_comment_client/base/views.py
@@ -21,11 +21,11 @@ from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
-from courseware.courses import get_cohort_id
+from courseware.courses import get_cohort_id,is_commentable_cohorted
from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context
-from django_comment_client.permissions import check_permissions_by_view
+from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
from django_comment_client.models import Role
def permitted(fn):
@@ -59,10 +59,17 @@ def ajax_content_response(request, course_id, content, template_name):
'annotated_content_info': annotated_content_info,
})
+
+
+def is_moderator(user, course_id):
+ cached_has_permission(user, "see_all_cohorts", course_id)
+
@require_POST
@login_required
@permitted
def create_thread(request, course_id, commentable_id):
+ print "\n\n\n\n\n*******************"
+ print commentable_id
course = get_course_with_access(request.user, course_id, 'load')
post = request.POST
@@ -85,23 +92,28 @@ def create_thread(request, course_id, commentable_id):
'user_id' : request.user.id,
})
- #now cohort id
+
+ #now cohort the thread if the commentable is cohorted
#if the group id came in from the form, set it there, otherwise,
#see if the user and the commentable are cohorted
- print post
+ if is_commentable_cohorted(course_id,commentable_id):
+ if 'group_id' in post: #if a group id was submitted in the form
+ posted_group_id = post['group_id']
+ else:
+ post_group_id = None
- group_id = None
-
- if 'group_id' in post:
- group_id = post['group_id']
-
-
- if group_id is None:
- group_id = get_cohort_id(request.user, course_id)
+ user_group_id = get_cohort_id(request.user, course_id)
- if group_id is not None:
+ if is_moderator(request.user,course_id):
+ if post_group_id is None:
+ group_id = user_group_id
+ else:
+ group_id = post_group_id
+ else:
+ group_id = user_group_id
+
thread.update_attributes(**{'group_id' :group_id})
-
+
thread.save()
if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user)
diff --git a/lms/djangoapps/django_comment_client/forum/urls.py b/lms/djangoapps/django_comment_client/forum/urls.py
index 526ae3e582..1e676dee87 100644
--- a/lms/djangoapps/django_comment_client/forum/urls.py
+++ b/lms/djangoapps/django_comment_client/forum/urls.py
@@ -4,7 +4,7 @@ import django_comment_client.forum.views
urlpatterns = patterns('django_comment_client.forum.views',
url(r'users/(?P\w+)/followed$', 'followed_threads', name='followed_threads'),
url(r'users/(?P\w+)$', 'user_profile', name='user_profile'),
- url(r'(?P[\w\-]+)/threads/(?P\w+)$', 'single_thread', name='single_thread'),
- url(r'(?P[\w\-]+)/inline$', 'inline_discussion', name='inline_discussion'),
+ url(r'^(?P[\w\-.]+)/threads/(?P\w+)$', 'single_thread', name='single_thread'),
+ url(r'^(?P[\w\-.]+)/inline$', 'inline_discussion', name='inline_discussion'),
url(r'', 'forum_form_discussion', name='forum_form_discussion'),
)
diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py
index 0e8a044097..443329ec1f 100644
--- a/lms/djangoapps/django_comment_client/forum/views.py
+++ b/lms/djangoapps/django_comment_client/forum/views.py
@@ -34,7 +34,6 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
This may raise cc.utils.CommentClientError or
cc.utils.CommentClientUnknownError if something goes wrong.
"""
-
default_query_params = {
'page': 1,
'per_page': per_page,
@@ -64,6 +63,8 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
group_id = get_cohort_id(user,course_id);
if group_id:
default_query_params["group_id"] = group_id;
+ print("\n\n\n\n\n****************GROUP ID IS ")
+ print group_id
query_params = merge_dict(default_query_params,
strip_none(extract(request.GET, ['page', 'sort_key', 'sort_order', 'text', 'tags', 'commentable_ids'])))
diff --git a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
index 3faa846033..958b67cdb3 100644
--- a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
+++ b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py
@@ -23,7 +23,7 @@ class Command(BaseCommand):
student_role.add_permission(per)
for per in ["edit_content", "delete_thread", "openclose_thread",
- "endorse_comment", "delete_comment"]:
+ "endorse_comment", "delete_comment", "see_all_cohorts"]:
moderator_role.add_permission(per)
for per in ["manage_moderator"]:
From 4f37ea91497068bc7c9ac9c28cf9bf8ec5bdf258 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Thu, 24 Jan 2013 14:45:31 -0500
Subject: [PATCH 36/82] add --create_dummy_exam option to
pearson_make_tc_registration
---
.../commands/pearson_make_tc_registration.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
index 81a478d19d..0f28ceb283 100644
--- a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
+++ b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
@@ -71,6 +71,12 @@ class Command(BaseCommand):
dest='ignore_registration_dates',
help='find exam info for course based on exam_series_code, even if the exam is not active.'
),
+ make_option(
+ '--create_dummy_exam',
+ action='store_true',
+ dest='create_dummy_exam',
+ help='create dummy exam info for course, even if course exists'
+ ),
)
args = ""
help = "Create or modify a TestCenterRegistration entry for a given Student"
@@ -99,6 +105,7 @@ class Command(BaseCommand):
raise CommandError("User \"{}\" does not have an existing demographics record".format(username))
# check to see if a course_id was specified, and use information from that:
+ create_dummy_exam = 'create_dummy_exam' in our_options and our_options['create_dummy_exam']
try:
course = course_from_id(course_id)
if 'ignore_registration_dates' in our_options:
@@ -107,6 +114,9 @@ class Command(BaseCommand):
else:
exam = course.current_test_center_exam
except ItemNotFoundError:
+ create_dummy_exam = True
+
+ if exam is None and create_dummy_exam:
# otherwise use explicit values (so we don't have to define a course):
exam_name = "Dummy Placeholder Name"
exam_info = { 'Exam_Series_Code': our_options['exam_series_code'],
@@ -120,7 +130,7 @@ class Command(BaseCommand):
our_options['eligibility_appointment_date_last'] = strftime("%Y-%m-%d", exam.last_eligible_appointment_date)
if exam is None:
- raise CommandError("Exam for course_id {%s} does not exist".format(course_id))
+ raise CommandError("Exam for course_id {} does not exist".format(course_id))
exam_code = exam.exam_series_code
From c655f62f7a8f0690efa910989aacc09b32ce02f2 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 19 Jan 2013 12:08:31 -0500
Subject: [PATCH 37/82] Remove out-of-date text about user replication, askbot
---
common/djangoapps/student/models.py | 27 +--------------------------
1 file changed, 1 insertion(+), 26 deletions(-)
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index f13a691215..00819d7dc4 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -1,30 +1,5 @@
"""
-Models for Student Information
-
-Replication Notes
-
-TODO: Update this to be consistent with reality (no portal servers, no more askbot)
-
-In our live deployment, we intend to run in a scenario where there is a pool of
-Portal servers that hold the canoncial user information and that user
-information is replicated to slave Course server pools. Each Course has a set of
-servers that serves only its content and has users that are relevant only to it.
-
-We replicate the following tables into the Course DBs where the user is
-enrolled. Only the Portal servers should ever write to these models.
-* UserProfile
-* CourseEnrollment
-
-We do a partial replication of:
-* User -- Askbot extends this and uses the extra fields, so we replicate only
- the stuff that comes with basic django_auth and ignore the rest.)
-
-There are a couple different scenarios:
-
-1. There's an update of User or UserProfile -- replicate it to all Course DBs
- that the user is enrolled in (found via CourseEnrollment).
-2. There's a change in CourseEnrollment. We need to push copies of UserProfile,
- CourseEnrollment, and the base fields in User
+Models for User Information (students, staff, etc)
Migration Notes
From 0e78eaaf80f21bbc5bdb76f019d4e33396c62a21 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 19 Jan 2013 13:20:34 -0500
Subject: [PATCH 38/82] Add a course_groups djangoapp with a CourseUserGroup
model.
---
common/djangoapps/course_groups/__init__.py | 0
common/djangoapps/course_groups/models.py | 27 +++++++++++++++++++++
lms/envs/common.py | 1 +
3 files changed, 28 insertions(+)
create mode 100644 common/djangoapps/course_groups/__init__.py
create mode 100644 common/djangoapps/course_groups/models.py
diff --git a/common/djangoapps/course_groups/__init__.py b/common/djangoapps/course_groups/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/common/djangoapps/course_groups/models.py b/common/djangoapps/course_groups/models.py
new file mode 100644
index 0000000000..5423b6ec16
--- /dev/null
+++ b/common/djangoapps/course_groups/models.py
@@ -0,0 +1,27 @@
+from django.contrib.auth.models import User
+from django.db import models
+
+class CourseUserGroup(models.Model):
+ """
+ This model represents groups of users in a course. Groups may have different types,
+ which may be treated specially. For example, a user can be in at most one cohort per
+ course, and cohorts are used to split up the forums by group.
+ """
+ class Meta:
+ unique_together = (('name', 'course_id'), )
+
+ name = models.CharField(max_length=255,
+ help_text=("What is the name of this group? "
+ "Must be unique within a course."))
+ users = models.ManyToManyField(User, db_index=True, related_name='course_groups',
+ help_text="Who is in this group?")
+
+ # Note: groups associated with particular runs of a course. E.g. Fall 2012 and Spring
+ # 2013 versions of 6.00x will have separate groups.
+ course_id = models.CharField(max_length=255, db_index=True,
+ help_text="Which course is this group associated with?")
+
+ # For now, only have group type 'cohort', but adding a type field to support
+ # things like 'question_discussion', 'friends', 'off-line-class', etc
+ GROUP_TYPE_CHOICES = (('cohort', 'Cohort'),)
+ group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 5e5ea86a4e..16472795e0 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -577,6 +577,7 @@ INSTALLED_APPS = (
'open_ended_grading',
'psychometrics',
'licenses',
+ 'course_groups',
#For the wiki
'wiki', # The new django-wiki from benjaoming
From 1b9a3557eb60b8be8539e6adcfb7e4a067b0cc37 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 19 Jan 2013 13:40:13 -0500
Subject: [PATCH 39/82] get_cohort() function and test
---
common/djangoapps/course_groups/models.py | 29 ++++++++++++++++++-
.../djangoapps/course_groups/tests/tests.py | 21 ++++++++++++++
2 files changed, 49 insertions(+), 1 deletion(-)
create mode 100644 common/djangoapps/course_groups/tests/tests.py
diff --git a/common/djangoapps/course_groups/models.py b/common/djangoapps/course_groups/models.py
index 5423b6ec16..36c9927e18 100644
--- a/common/djangoapps/course_groups/models.py
+++ b/common/djangoapps/course_groups/models.py
@@ -23,5 +23,32 @@ class CourseUserGroup(models.Model):
# For now, only have group type 'cohort', but adding a type field to support
# things like 'question_discussion', 'friends', 'off-line-class', etc
- GROUP_TYPE_CHOICES = (('cohort', 'Cohort'),)
+ COHORT = 'cohort'
+ GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
+
+
+def get_cohort(user, course_id):
+ """
+ Given a django User and a course_id, return the user's cohort. In classes with
+ auto-cohorting, put the user in a cohort if they aren't in one already.
+
+ Arguments:
+ user: a Django User object.
+ course_id: string in the format 'org/course/run'
+
+ Returns:
+ A CourseUserGroup object if the User has a cohort, or None.
+ """
+ group_type = CourseUserGroup.COHORT
+ try:
+ group = CourseUserGroup.objects.get(course_id=course_id, group_type=group_type,
+ users__id=user.id)
+ except CourseUserGroup.DoesNotExist:
+ group = None
+
+ if group:
+ return group
+
+ # TODO: add auto-cohorting logic here
+ return None
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
new file mode 100644
index 0000000000..89c77c5b65
--- /dev/null
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -0,0 +1,21 @@
+from django.contrib.auth.models import User
+from nose.tools import assert_equals
+
+from course_groups.models import CourseUserGroup, get_cohort
+
+def test_get_cohort():
+ course_id = "a/b/c"
+ cohort = CourseUserGroup.objects.create(name="TestCohort", course_id=course_id,
+ group_type=CourseUserGroup.COHORT)
+
+ user = User.objects.create(username="test", email="a@b.com")
+ other_user = User.objects.create(username="test2", email="a2@b.com")
+
+ cohort.users.add(user)
+
+ got = get_cohort(user, course_id)
+ assert_equals(got.id, cohort.id, "Should find the right cohort")
+
+ got = get_cohort(other_user, course_id)
+ assert_equals(got, None, "other_user shouldn't have a cohort")
+
From fa17913a91e4906e1fb53563cb3a4dfac69f041c Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 19 Jan 2013 14:17:35 -0500
Subject: [PATCH 40/82] is_cohorted course descriptor property, docs
---
common/lib/xmodule/xmodule/course_module.py | 11 +++++++++++
doc/xml-format.md | 4 ++++
2 files changed, 15 insertions(+)
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 5416dae583..c0b3000258 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -360,6 +360,17 @@ class CourseDescriptor(SequenceDescriptor):
def show_calculator(self):
return self.metadata.get("show_calculator", None) == "Yes"
+ @property
+ def is_cohorted(self):
+ """
+ Return whether the course is cohorted.
+ """
+ config = self.metadata.get("cohort-config")
+ if config is None:
+ return False
+
+ return bool(config.get("cohorted"))
+
@property
def is_new(self):
"""
diff --git a/doc/xml-format.md b/doc/xml-format.md
index 8138de4d7e..8f9e512ac1 100644
--- a/doc/xml-format.md
+++ b/doc/xml-format.md
@@ -258,6 +258,10 @@ Supported fields at the course level:
* "discussion_blackouts" -- An array of time intervals during which you want to disable a student's ability to create or edit posts in the forum. Moderators, Community TAs, and Admins are unaffected. You might use this during exam periods, but please be aware that the forum is often a very good place to catch mistakes and clarify points to students. The better long term solution would be to have better flagging/moderation mechanisms, but this is the hammer we have today. Format by example: [["2012-10-29T04:00", "2012-11-03T04:00"], ["2012-12-30T04:00", "2013-01-02T04:00"]]
* "show_calculator" (value "Yes" if desired)
* "days_early_for_beta" -- number of days (floating point ok) early that students in the beta-testers group get to see course content. Can also be specified for any other course element, and overrides values set at higher levels.
+* "cohort-config" : dictionary with keys
+ - "cohorted" : boolean. Set to true if this course uses student cohorts. If so, all inline discussions are automatically cohorted, and top-level discussion topics are configurable with an optional 'cohorted': bool parameter (with default value false).
+ - ... more to come. ('auto-cohort', how to auto cohort, etc)
+
* TODO: there are others
### Grading policy file contents
From f005b70f3b8b5b4c68519c22f85fa153a173a212 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 19 Jan 2013 14:17:58 -0500
Subject: [PATCH 41/82] Minor test cleanups
---
.../lib/xmodule/xmodule/tests/test_import.py | 32 +++++++++----------
1 file changed, 15 insertions(+), 17 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index 554e89ac74..8fc8916dc3 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -52,6 +52,16 @@ class ImportTestCase(unittest.TestCase):
'''Get a dummy system'''
return DummySystem(load_error_modules)
+ def get_course(self, name):
+ """Get a test course by directory name. If there's more than one, error."""
+ print "Importing {0}".format(name)
+
+ modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
+ courses = modulestore.get_courses()
+ self.assertEquals(len(courses), 1)
+ return courses[0]
+
+
def test_fallback(self):
'''Check that malformed xml loads as an ErrorDescriptor.'''
@@ -207,11 +217,7 @@ class ImportTestCase(unittest.TestCase):
"""Make sure that metadata is inherited properly"""
print "Starting import"
- initial_import = XMLModuleStore(DATA_DIR, course_dirs=['toy'])
-
- courses = initial_import.get_courses()
- self.assertEquals(len(courses), 1)
- course = courses[0]
+ course = self.get_course('toy')
def check_for_key(key, node):
"recursive check for presence of key"
@@ -227,16 +233,8 @@ class ImportTestCase(unittest.TestCase):
"""Make sure that when two courses share content with the same
org and course names, policy applies to the right one."""
- def get_course(name):
- print "Importing {0}".format(name)
-
- modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
- courses = modulestore.get_courses()
- self.assertEquals(len(courses), 1)
- return courses[0]
-
- toy = get_course('toy')
- two_toys = get_course('two_toys')
+ toy = self.get_course('toy')
+ two_toys = self.get_course('two_toys')
self.assertEqual(toy.url_name, "2012_Fall")
self.assertEqual(two_toys.url_name, "TT_2012_Fall")
@@ -279,8 +277,8 @@ class ImportTestCase(unittest.TestCase):
"""Ensure that colons in url_names convert to file paths properly"""
print "Starting import"
+ # Not using get_courses because we need the modulestore object too afterward
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy'])
-
courses = modulestore.get_courses()
self.assertEquals(len(courses), 1)
course = courses[0]
@@ -317,7 +315,7 @@ class ImportTestCase(unittest.TestCase):
toy_id = "edX/toy/2012_Fall"
- course = modulestore.get_courses()[0]
+ course = modulestore.get_course(toy_id)
chapters = course.get_children()
ch1 = chapters[0]
sections = ch1.get_children()
From 90a5703419b9322eb3d5c8130af6a655b938cb3e Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 19 Jan 2013 14:18:06 -0500
Subject: [PATCH 42/82] Test for cohort config
---
.../lib/xmodule/xmodule/tests/test_import.py | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index 8fc8916dc3..8a5eda3882 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -353,3 +353,28 @@ class ImportTestCase(unittest.TestCase):
\
""".strip()
self.assertEqual(gst_sample.definition['render'], render_string_from_sample_gst_xml)
+
+ def test_cohort_config(self):
+ """
+ Check that cohort config parsing works right.
+ """
+ modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy'])
+
+ toy_id = "edX/toy/2012_Fall"
+
+ course = modulestore.get_course(toy_id)
+
+ # No config -> False
+ self.assertFalse(course.is_cohorted)
+
+ # empty config -> False
+ course.metadata['cohort-config'] = {}
+ self.assertFalse(course.is_cohorted)
+
+ # false config -> False
+ course.metadata['cohort-config'] = {'cohorted': False}
+ self.assertFalse(course.is_cohorted)
+
+ # and finally...
+ course.metadata['cohort-config'] = {'cohorted': True}
+ self.assertTrue(course.is_cohorted)
From 3488083e6bccbfbfcb3833350d8c7f4e2c2a2962 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 19 Jan 2013 14:58:46 -0500
Subject: [PATCH 43/82] Add a get_course_cohorts function and test
---
common/djangoapps/course_groups/models.py | 21 ++++++--
.../djangoapps/course_groups/tests/tests.py | 49 ++++++++++++++-----
2 files changed, 53 insertions(+), 17 deletions(-)
diff --git a/common/djangoapps/course_groups/models.py b/common/djangoapps/course_groups/models.py
index 36c9927e18..46859a900e 100644
--- a/common/djangoapps/course_groups/models.py
+++ b/common/djangoapps/course_groups/models.py
@@ -40,15 +40,28 @@ def get_cohort(user, course_id):
Returns:
A CourseUserGroup object if the User has a cohort, or None.
"""
- group_type = CourseUserGroup.COHORT
try:
- group = CourseUserGroup.objects.get(course_id=course_id, group_type=group_type,
- users__id=user.id)
+ group = CourseUserGroup.objects.get(course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ users__id=user.id)
except CourseUserGroup.DoesNotExist:
group = None
-
+
if group:
return group
# TODO: add auto-cohorting logic here
return None
+
+def get_course_cohorts(course_id):
+ """
+ Get a list of all the cohorts in the given course.
+
+ Arguments:
+ course_id: string in the format 'org/course/run'
+
+ Returns:
+ A list of CourseUserGroup objects. Empty if there are no cohorts.
+ """
+ return list(CourseUserGroup.objects.filter(course_id=course_id,
+ group_type=CourseUserGroup.COHORT))
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
index 89c77c5b65..676643567d 100644
--- a/common/djangoapps/course_groups/tests/tests.py
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -1,21 +1,44 @@
+import django.test
from django.contrib.auth.models import User
-from nose.tools import assert_equals
-from course_groups.models import CourseUserGroup, get_cohort
+from course_groups.models import CourseUserGroup, get_cohort, get_course_cohorts
-def test_get_cohort():
- course_id = "a/b/c"
- cohort = CourseUserGroup.objects.create(name="TestCohort", course_id=course_id,
- group_type=CourseUserGroup.COHORT)
+class TestCohorts(django.test.TestCase):
- user = User.objects.create(username="test", email="a@b.com")
- other_user = User.objects.create(username="test2", email="a2@b.com")
+ def test_get_cohort(self):
+ course_id = "a/b/c"
+ cohort = CourseUserGroup.objects.create(name="TestCohort", course_id=course_id,
+ group_type=CourseUserGroup.COHORT)
- cohort.users.add(user)
+ user = User.objects.create(username="test", email="a@b.com")
+ other_user = User.objects.create(username="test2", email="a2@b.com")
- got = get_cohort(user, course_id)
- assert_equals(got.id, cohort.id, "Should find the right cohort")
+ cohort.users.add(user)
- got = get_cohort(other_user, course_id)
- assert_equals(got, None, "other_user shouldn't have a cohort")
+ got = get_cohort(user, course_id)
+ self.assertEquals(got.id, cohort.id, "Should find the right cohort")
+
+ got = get_cohort(other_user, course_id)
+ self.assertEquals(got, None, "other_user shouldn't have a cohort")
+
+
+ def test_get_course_cohorts(self):
+ course1_id = "a/b/c"
+ course2_id = "e/f/g"
+
+ # add some cohorts to course 1
+ cohort = CourseUserGroup.objects.create(name="TestCohort",
+ course_id=course1_id,
+ group_type=CourseUserGroup.COHORT)
+
+ cohort = CourseUserGroup.objects.create(name="TestCohort2",
+ course_id=course1_id,
+ group_type=CourseUserGroup.COHORT)
+
+
+ # second course should have no cohorts
+ self.assertEqual(get_course_cohorts(course2_id), [])
+
+ cohorts = sorted([c.name for c in get_course_cohorts(course1_id)])
+ self.assertEqual(cohorts, ['TestCohort', 'TestCohort2'])
From 0d41a04490a20deac30e1216e63982e8940d88e2 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 19 Jan 2013 15:00:43 -0500
Subject: [PATCH 44/82] fix line length
---
common/lib/xmodule/xmodule/discussion_module.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/common/lib/xmodule/xmodule/discussion_module.py b/common/lib/xmodule/xmodule/discussion_module.py
index 1deceac5d0..57d7780d95 100644
--- a/common/lib/xmodule/xmodule/discussion_module.py
+++ b/common/lib/xmodule/xmodule/discussion_module.py
@@ -18,8 +18,10 @@ class DiscussionModule(XModule):
}
return self.system.render_template('discussion/_discussion_module.html', context)
- def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
+ def __init__(self, system, location, definition, descriptor,
+ instance_state=None, shared_state=None, **kwargs):
+ XModule.__init__(self, system, location, definition, descriptor,
+ instance_state, shared_state, **kwargs)
if isinstance(instance_state, str):
instance_state = json.loads(instance_state)
From ac8c59126d40e4fd7d70fc4113634885bcbd67c0 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 19 Jan 2013 15:05:41 -0500
Subject: [PATCH 45/82] Add note about non-unique topic id across course runs
bug
---
lms/djangoapps/django_comment_client/utils.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py
index 3094367491..3c9669ac37 100644
--- a/lms/djangoapps/django_comment_client/utils.py
+++ b/lms/djangoapps/django_comment_client/utils.py
@@ -207,6 +207,9 @@ def initialize_discussion_info(course):
"sort_key": entry["sort_key"],
"start_date": entry["start_date"]}
+ # TODO. BUG! : course location is not unique across multiple course runs!
+ # (I think Kevin already noticed this) Need to send course_id with requests, store it
+ # in the backend.
default_topics = {'General': {'id' :course.location.html_id()}}
discussion_topics = course.metadata.get('discussion_topics', default_topics)
for topic, entry in discussion_topics.items():
From bab3b0c39e94b6e060b80e6e31a41a090bb3f972 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Mon, 21 Jan 2013 15:48:58 -0500
Subject: [PATCH 46/82] Add initial set of views for managing cohorts
- lets user see and add cohorts
- needs styling
- needs to allow them to actually manage membership!
---
common/djangoapps/course_groups/models.py | 51 +++++++-
common/djangoapps/course_groups/views.py | 108 ++++++++++++++++
common/static/js/course_groups/cohorts.js | 118 ++++++++++++++++++
lms/djangoapps/instructor/views.py | 1 +
.../course_groups/cohort_management.html | 25 ++++
lms/templates/course_groups/debug.html | 16 +++
.../courseware/instructor_dashboard.html | 7 +-
lms/urls.py | 17 +++
8 files changed, 341 insertions(+), 2 deletions(-)
create mode 100644 common/djangoapps/course_groups/views.py
create mode 100644 common/static/js/course_groups/cohorts.js
create mode 100644 lms/templates/course_groups/cohort_management.html
create mode 100644 lms/templates/course_groups/debug.html
diff --git a/common/djangoapps/course_groups/models.py b/common/djangoapps/course_groups/models.py
index 46859a900e..701cce0e6c 100644
--- a/common/djangoapps/course_groups/models.py
+++ b/common/djangoapps/course_groups/models.py
@@ -1,6 +1,10 @@
+import logging
+
from django.contrib.auth.models import User
from django.db import models
+log = logging.getLogger(__name__)
+
class CourseUserGroup(models.Model):
"""
This model represents groups of users in a course. Groups may have different types,
@@ -27,7 +31,6 @@ class CourseUserGroup(models.Model):
GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
-
def get_cohort(user, course_id):
"""
Given a django User and a course_id, return the user's cohort. In classes with
@@ -65,3 +68,49 @@ def get_course_cohorts(course_id):
"""
return list(CourseUserGroup.objects.filter(course_id=course_id,
group_type=CourseUserGroup.COHORT))
+
+### Helpers for cohor management views
+
+def get_cohort_by_name(course_id, name):
+ """
+ Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
+ it isn't present.
+ """
+ return CourseUserGroup.objects.get(course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ name=name)
+
+def add_cohort(course_id, name):
+ """
+ Add a cohort to a course. Raises ValueError if a cohort of the same name already
+ exists.
+ """
+ log.debug("Adding cohort %s to %s", name, course_id)
+ if CourseUserGroup.objects.filter(course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ name=name).exists():
+ raise ValueError("Can't create two cohorts with the same name")
+
+ return CourseUserGroup.objects.create(course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ name=name)
+
+def get_course_cohort_names(course_id):
+ """
+ Return a list of the cohort names in a course.
+ """
+ return [c.name for c in get_course_cohorts(course_id)]
+
+
+def delete_empty_cohort(course_id, name):
+ """
+ Remove an empty cohort. Raise ValueError if cohort is not empty.
+ """
+ cohort = get_cohort_by_name(course_id, name)
+ if cohort.users.exists():
+ raise ValueError(
+ "Can't delete non-empty cohort {0} in course {1}".format(
+ name, course_id))
+
+ cohort.delete()
+
diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py
new file mode 100644
index 0000000000..9ee9935c3e
--- /dev/null
+++ b/common/djangoapps/course_groups/views.py
@@ -0,0 +1,108 @@
+import json
+from django_future.csrf import ensure_csrf_cookie
+from django.contrib.auth.decorators import login_required
+from django.core.context_processors import csrf
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse, HttpResponseForbidden, Http404
+from django.shortcuts import redirect
+import logging
+
+from courseware.courses import get_course_with_access
+from mitxmako.shortcuts import render_to_response, render_to_string
+from .models import CourseUserGroup
+from . import models
+
+import track.views
+
+
+log = logging.getLogger(__name__)
+
+def JsonHttpReponse(data):
+ """
+ Return an HttpResponse with the data json-serialized and the right content type
+ header. Named to look like a class.
+ """
+ return HttpResponse(json.dumps(data), content_type="application/json")
+
+@ensure_csrf_cookie
+def list_cohorts(request, course_id):
+ """
+ Return json dump of dict:
+
+ {'success': True,
+ 'cohorts': [{'name': name, 'id': id}, ...]}
+ """
+ get_course_with_access(request.user, course_id, 'staff')
+
+ cohorts = [{'name': c.name, 'id': c.id}
+ for c in models.get_course_cohorts(course_id)]
+
+ return JsonHttpReponse({'success': True,
+ 'cohorts': cohorts})
+
+
+@ensure_csrf_cookie
+def add_cohort(request, course_id):
+ """
+ Return json of dict:
+ {'success': True,
+ 'cohort': {'id': id,
+ 'name': name}}
+
+ or
+
+ {'success': False,
+ 'msg': error_msg} if there's an error
+ """
+ get_course_with_access(request.user, course_id, 'staff')
+
+
+ if request.method != "POST":
+ raise Http404("Must POST to add cohorts")
+
+ name = request.POST.get("name")
+ if not name:
+ return JsonHttpReponse({'success': False,
+ 'msg': "No name specified"})
+
+ try:
+ cohort = models.add_cohort(course_id, name)
+ except ValueError as err:
+ return JsonHttpReponse({'success': False,
+ 'msg': str(err)})
+
+ return JsonHttpReponse({'success': 'True',
+ 'cohort': {
+ 'id': cohort.id,
+ 'name': cohort.name
+ }})
+
+
+@ensure_csrf_cookie
+def users_in_cohort(request, course_id, cohort_id):
+ """
+ """
+ get_course_with_access(request.user, course_id, 'staff')
+
+ return JsonHttpReponse({'error': 'Not implemented'})
+
+
+@ensure_csrf_cookie
+def add_users_to_cohort(request, course_id):
+ """
+ """
+ get_course_with_access(request.user, course_id, 'staff')
+
+ return JsonHttpReponse({'error': 'Not implemented'})
+
+
+def debug_cohort_mgmt(request, course_id):
+ """
+ Debugging view for dev.
+ """
+ # add staff check to make sure it's safe if it's accidentally deployed.
+ get_course_with_access(request.user, course_id, 'staff')
+
+ context = {'cohorts_ajax_url': reverse('cohorts',
+ kwargs={'course_id': course_id})}
+ return render_to_response('/course_groups/debug.html', context)
diff --git a/common/static/js/course_groups/cohorts.js b/common/static/js/course_groups/cohorts.js
new file mode 100644
index 0000000000..7b1793dcf8
--- /dev/null
+++ b/common/static/js/course_groups/cohorts.js
@@ -0,0 +1,118 @@
+// structure stolen from http://briancray.com/posts/javascript-module-pattern
+
+var CohortManager = (function ($) {
+ // private variables and functions
+
+ // using jQuery
+ function getCookie(name) {
+ var cookieValue = null;
+ if (document.cookie && document.cookie != '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = $.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ }
+ var csrftoken = getCookie('csrftoken');
+
+ function csrfSafeMethod(method) {
+ // these HTTP methods do not require CSRF protection
+ return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+ }
+ $.ajaxSetup({
+ crossDomain: false, // obviates need for sameOrigin test
+ beforeSend: function(xhr, settings) {
+ if (!csrfSafeMethod(settings.type)) {
+ xhr.setRequestHeader("X-CSRFToken", csrftoken);
+ }
+ }
+ });
+
+ // constructor
+ var module = function () {
+ var url = $(".cohort_manager").data('ajax_url');
+ var self = this;
+ var error_list = $(".cohort_errors");
+ var cohort_list = $(".cohort_list");
+ var cohorts_display = $(".cohorts_display");
+ var show_cohorts_button = $(".cohort_controls .show_cohorts");
+ var add_cohort_input = $("#cohort-name");
+ var add_cohort_button = $(".add_cohort");
+
+ function show_cohort(item) {
+ // item is a li that has a data-href link to the cohort base url
+ var el = $(this);
+ alert("would show you data about " + el.text() + " from " + el.data('href'));
+ }
+
+ function add_to_cohorts_list(item) {
+ var li = $('
");
+ if (response && response.success) {
+ response.users.forEach(add_to_users_list);
+ detail_page_num.text("Page " + response.page + " of " + response.num_pages);
+ } else {
+ log_error(response.msg ||
+ "There was an error loading users for " + cohort.title);
+ }
+ detail.show();
+ }
+
+
+ function added_users(response) {
+ function adder(note, color) {
+ return function(item) {
+ var li = $('')
+ li.text(note + ' ' + item.name + ', ' + item.username + ', ' + item.email);
+ li.css('color', color);
+ op_results.append(li);
+ }
+ }
+ if (response && response.success) {
+ response.added.forEach(adder("Added", "green"));
+ response.present.forEach(adder("Already present:", "black"));
+ response.unknown.forEach(adder("Already present:", "red"));
+ } else {
+ log_error(response.msg || "There was an error adding users");
+ }
+ }
+
+ // ******* Rendering
+
+
+ function render() {
+ // Load and render the right thing based on the state
+
+ // start with both divs hidden
+ summary.hide();
+ detail.hide();
+ // and clear out the errors
+ errors.empty();
+ if (state == state_summary) {
+ $.ajax(url).done(load_cohorts).fail(function() {
+ log_error("Error trying to load cohorts");
+ });
+ } else if (state == state_detail) {
+ detail_header.text("Members of " + cohort_title);
+ $.ajax(detail_url).done(show_users).fail(function() {
+ log_error("Error trying to load users in cohort");
+ });
+ }
+ }
+
show_cohorts_button.click(function() {
- $.ajax(url).done(load_cohorts);
+ state = state_summary;
+ render();
});
add_cohort_input.change(function() {
@@ -101,6 +201,13 @@ var CohortManager = (function ($) {
$.post(add_url, data).done(added_cohort);
});
+ add_members_button.click(function() {
+ var add_url = detail_url + '/add';
+ data = {'users': users_area.val()}
+ $.post(add_url, data).done(added_users);
+ });
+
+
};
// prototype
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 8069d3f184..1b51698834 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -24,14 +24,15 @@ from courseware import grades
from courseware.access import (has_access, get_access_group_name,
course_beta_test_group_name)
from courseware.courses import get_course_with_access
+from courseware.models import StudentModule
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 string_util import split_by_comma_and_whitespace
from student.models import CourseEnrollment, CourseEnrollmentAllowed
-from courseware.models import StudentModule
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
@@ -392,14 +393,14 @@ def instructor_dashboard(request, course_id):
users = request.POST['betausers']
log.debug("users: {0!r}".format(users))
group = get_beta_group(course)
- for username_or_email in _split_by_comma_and_whitespace(users):
+ for username_or_email in split_by_comma_and_whitespace(users):
msg += "
{0}
".format(
add_user_to_group(request, username_or_email, group, 'beta testers', 'beta-tester'))
elif action == 'Remove beta testers':
users = request.POST['betausers']
group = get_beta_group(course)
- for username_or_email in _split_by_comma_and_whitespace(users):
+ for username_or_email in split_by_comma_and_whitespace(users):
msg += "
{0}
".format(
remove_user_from_group(request, username_or_email, group, 'beta testers', 'beta-tester'))
@@ -871,21 +872,11 @@ def grade_summary(request, course_id):
#-----------------------------------------------------------------------------
# enrollment
-
-def _split_by_comma_and_whitespace(s):
- """
- Split a string both by on commas and whitespice.
- """
- # Note: split() with no args removes empty strings from output
- lists = [x.split() for x in s.split(',')]
- # return all of them
- return itertools.chain(*lists)
-
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"""
- new_students = _split_by_comma_and_whitespace(students)
+ new_students = split_by_comma_and_whitespace(students)
new_students = [str(s.strip()) for s in new_students]
new_students_lc = [x.lower() for x in new_students]
diff --git a/lms/templates/course_groups/cohort_management.html b/lms/templates/course_groups/cohort_management.html
index 1512d09689..962d4de645 100644
--- a/lms/templates/course_groups/cohort_management.html
+++ b/lms/templates/course_groups/cohort_management.html
@@ -1,24 +1,40 @@
From 9e84ae14d4c32509d4c0de74b632a8c7b664def2 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Wed, 23 Jan 2013 17:49:01 -0500
Subject: [PATCH 49/82] Started integration work with Kevin's cohort changes
- Create new cohorts.py file
- moved code there from models.py
- implemented necessary functions.
Next: testing :)
---
common/djangoapps/course_groups/cohorts.py | 166 ++++++++++++++++++
common/djangoapps/course_groups/models.py | 119 -------------
common/djangoapps/course_groups/views.py | 18 +-
common/lib/xmodule/xmodule/course_module.py | 33 ++--
lms/djangoapps/courseware/courses.py | 20 ---
.../django_comment_client/base/views.py | 2 +-
.../django_comment_client/forum/views.py | 2 +-
7 files changed, 200 insertions(+), 160 deletions(-)
create mode 100644 common/djangoapps/course_groups/cohorts.py
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
new file mode 100644
index 0000000000..6c18491775
--- /dev/null
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -0,0 +1,166 @@
+"""
+This file contains the logic for cohort groups, as exposed internally to the
+forums, and to the cohort admin views.
+"""
+
+from django.contrib.auth.models import User
+import logging
+
+from courseware import courses
+from .models import CourseUserGroup
+
+log = logging.getLogger(__name__)
+
+def is_course_cohorted(course_id):
+ """
+ Given a course id, return a boolean for whether or not the course is
+ cohorted.
+
+ Raises:
+ Http404 if the course doesn't exist.
+ """
+ return courses.get_course_by_id(course_id).is_cohorted
+
+
+def get_cohort_id(user, course_id):
+ """
+ Given a course id and a user, return the id of the cohort that user is
+ assigned to in that course. If they don't have a cohort, return None.
+ """
+ cohort = get_cohort(user, course_id)
+ return None if cohort is None else cohort.id
+
+
+def is_commentable_cohorted(course_id, commentable_id):
+ """
+ Given a course and a commentable id, return whether or not this commentable
+ is cohorted.
+
+ Raises:
+ Http404 if the course doesn't exist.
+ """
+ course = courses.get_course_by_id(course_id)
+ return commentable_id in course.cohorted_discussions()
+
+
+def get_cohort(user, course_id):
+ """
+ Given a django User and a course_id, return the user's cohort. In classes with
+ auto-cohorting, put the user in a cohort if they aren't in one already.
+
+ Arguments:
+ user: a Django User object.
+ course_id: string in the format 'org/course/run'
+
+ Returns:
+ A CourseUserGroup object if the User has a cohort, or None.
+ """
+ try:
+ group = CourseUserGroup.objects.get(course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ users__id=user.id)
+ except CourseUserGroup.DoesNotExist:
+ group = None
+
+ if group:
+ return group
+
+ # TODO: add auto-cohorting logic here once we know what that will be.
+ return None
+
+
+def get_course_cohorts(course_id):
+ """
+ Get a list of all the cohorts in the given course.
+
+ Arguments:
+ course_id: string in the format 'org/course/run'
+
+ Returns:
+ A list of CourseUserGroup objects. Empty if there are no cohorts.
+ """
+ return list(CourseUserGroup.objects.filter(course_id=course_id,
+ group_type=CourseUserGroup.COHORT))
+
+### Helpers for cohort management views
+
+def get_cohort_by_name(course_id, name):
+ """
+ Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
+ it isn't present.
+ """
+ return CourseUserGroup.objects.get(course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ name=name)
+
+def get_cohort_by_id(course_id, cohort_id):
+ """
+ Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
+ it isn't present. Uses the course_id for extra validation...
+ """
+ return CourseUserGroup.objects.get(course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ id=cohort_id)
+
+def add_cohort(course_id, name):
+ """
+ Add a cohort to a course. Raises ValueError if a cohort of the same name already
+ exists.
+ """
+ log.debug("Adding cohort %s to %s", name, course_id)
+ if CourseUserGroup.objects.filter(course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ name=name).exists():
+ raise ValueError("Can't create two cohorts with the same name")
+
+ return CourseUserGroup.objects.create(course_id=course_id,
+ group_type=CourseUserGroup.COHORT,
+ name=name)
+
+def add_user_to_cohort(cohort, username_or_email):
+ """
+ Look up the given user, and if successful, add them to the specified cohort.
+
+ Arguments:
+ cohort: CourseUserGroup
+ username_or_email: string. Treated as email if has '@'
+
+ Returns:
+ User object.
+
+ Raises:
+ User.DoesNotExist if can't find user.
+
+ ValueError if user already present.
+ """
+ if '@' in username_or_email:
+ user = User.objects.get(email=username_or_email)
+ else:
+ user = User.objects.get(username=username_or_email)
+
+ if cohort.users.filter(id=user.id).exists():
+ raise ValueError("User {0} already present".format(user.username))
+
+ cohort.users.add(user)
+ return user
+
+
+def get_course_cohort_names(course_id):
+ """
+ Return a list of the cohort names in a course.
+ """
+ return [c.name for c in get_course_cohorts(course_id)]
+
+
+def delete_empty_cohort(course_id, name):
+ """
+ Remove an empty cohort. Raise ValueError if cohort is not empty.
+ """
+ cohort = get_cohort_by_name(course_id, name)
+ if cohort.users.exists():
+ raise ValueError(
+ "Can't delete non-empty cohort {0} in course {1}".format(
+ name, course_id))
+
+ cohort.delete()
+
diff --git a/common/djangoapps/course_groups/models.py b/common/djangoapps/course_groups/models.py
index dd46e5a055..957d230d92 100644
--- a/common/djangoapps/course_groups/models.py
+++ b/common/djangoapps/course_groups/models.py
@@ -31,123 +31,4 @@ class CourseUserGroup(models.Model):
GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
-def get_cohort(user, course_id):
- """
- Given a django User and a course_id, return the user's cohort. In classes with
- auto-cohorting, put the user in a cohort if they aren't in one already.
-
- Arguments:
- user: a Django User object.
- course_id: string in the format 'org/course/run'
-
- Returns:
- A CourseUserGroup object if the User has a cohort, or None.
- """
- try:
- group = CourseUserGroup.objects.get(course_id=course_id,
- group_type=CourseUserGroup.COHORT,
- users__id=user.id)
- except CourseUserGroup.DoesNotExist:
- group = None
-
- if group:
- return group
-
- # TODO: add auto-cohorting logic here
- return None
-
-def get_course_cohorts(course_id):
- """
- Get a list of all the cohorts in the given course.
-
- Arguments:
- course_id: string in the format 'org/course/run'
-
- Returns:
- A list of CourseUserGroup objects. Empty if there are no cohorts.
- """
- return list(CourseUserGroup.objects.filter(course_id=course_id,
- group_type=CourseUserGroup.COHORT))
-
-### Helpers for cohor management views
-
-def get_cohort_by_name(course_id, name):
- """
- Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
- it isn't present.
- """
- return CourseUserGroup.objects.get(course_id=course_id,
- group_type=CourseUserGroup.COHORT,
- name=name)
-
-def get_cohort_by_id(course_id, cohort_id):
- """
- Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
- it isn't present. Uses the course_id for extra validation...
- """
- return CourseUserGroup.objects.get(course_id=course_id,
- group_type=CourseUserGroup.COHORT,
- id=cohort_id)
-
-def add_cohort(course_id, name):
- """
- Add a cohort to a course. Raises ValueError if a cohort of the same name already
- exists.
- """
- log.debug("Adding cohort %s to %s", name, course_id)
- if CourseUserGroup.objects.filter(course_id=course_id,
- group_type=CourseUserGroup.COHORT,
- name=name).exists():
- raise ValueError("Can't create two cohorts with the same name")
-
- return CourseUserGroup.objects.create(course_id=course_id,
- group_type=CourseUserGroup.COHORT,
- name=name)
-
-def add_user_to_cohort(cohort, username_or_email):
- """
- Look up the given user, and if successful, add them to the specified cohort.
-
- Arguments:
- cohort: CourseUserGroup
- username_or_email: string. Treated as email if has '@'
-
- Returns:
- User object.
-
- Raises:
- User.DoesNotExist if can't find user.
-
- ValueError if user already present.
- """
- if '@' in username_or_email:
- user = User.objects.get(email=username_or_email)
- else:
- user = User.objects.get(username=username_or_email)
-
- if cohort.users.filter(id=user.id).exists():
- raise ValueError("User {0} already present".format(user.username))
-
- cohort.users.add(user)
- return user
-
-
-def get_course_cohort_names(course_id):
- """
- Return a list of the cohort names in a course.
- """
- return [c.name for c in get_course_cohorts(course_id)]
-
-
-def delete_empty_cohort(course_id, name):
- """
- Remove an empty cohort. Raise ValueError if cohort is not empty.
- """
- cohort = get_cohort_by_name(course_id, name)
- if cohort.users.exists():
- raise ValueError(
- "Can't delete non-empty cohort {0} in course {1}".format(
- name, course_id))
-
- cohort.delete()
diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py
index f02bff2d00..c79839208e 100644
--- a/common/djangoapps/course_groups/views.py
+++ b/common/djangoapps/course_groups/views.py
@@ -1,4 +1,3 @@
-import json
from django_future.csrf import ensure_csrf_cookie
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
@@ -7,6 +6,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseForbidden, Http404
from django.shortcuts import redirect
+import json
import logging
from courseware.courses import get_course_with_access
@@ -14,7 +14,7 @@ from mitxmako.shortcuts import render_to_response, render_to_string
from string_util import split_by_comma_and_whitespace
from .models import CourseUserGroup
-from . import models
+from . import cohorts
import track.views
@@ -38,11 +38,11 @@ def list_cohorts(request, course_id):
"""
get_course_with_access(request.user, course_id, 'staff')
- cohorts = [{'name': c.name, 'id': c.id}
- for c in models.get_course_cohorts(course_id)]
+ all_cohorts = [{'name': c.name, 'id': c.id}
+ for c in cohorts.get_course_cohorts(course_id)]
return JsonHttpReponse({'success': True,
- 'cohorts': cohorts})
+ 'cohorts': all_cohorts})
@ensure_csrf_cookie
@@ -70,7 +70,7 @@ def add_cohort(request, course_id):
'msg': "No name specified"})
try:
- cohort = models.add_cohort(course_id, name)
+ cohort = cohorts.add_cohort(course_id, name)
except ValueError as err:
return JsonHttpReponse({'success': False,
'msg': str(err)})
@@ -98,7 +98,7 @@ def users_in_cohort(request, course_id, cohort_id):
"""
get_course_with_access(request.user, course_id, 'staff')
- cohort = models.get_cohort_by_id(course_id, int(cohort_id))
+ cohort = cohorts.get_cohort_by_id(course_id, int(cohort_id))
paginator = Paginator(cohort.users.all(), 100)
page = request.GET.get('page')
@@ -141,7 +141,7 @@ def add_users_to_cohort(request, course_id, cohort_id):
if request.method != "POST":
raise Http404("Must POST to add users to cohorts")
- cohort = models.get_cohort_by_id(course_id, cohort_id)
+ cohort = cohorts.get_cohort_by_id(course_id, cohort_id)
users = request.POST.get('users', '')
added = []
@@ -149,7 +149,7 @@ def add_users_to_cohort(request, course_id, cohort_id):
unknown = []
for username_or_email in split_by_comma_and_whitespace(users):
try:
- user = models.add_user_to_cohort(cohort, username_or_email)
+ user = cohorts.add_user_to_cohort(cohort, username_or_email)
added.append({'username': user.username,
'name': "{0} {1}".format(user.first_name, user.last_name),
'email': user.email,
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index c0b3000258..5b6de04f3e 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -223,7 +223,7 @@ class CourseDescriptor(SequenceDescriptor):
return policy_str
-
+
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
instance = super(CourseDescriptor, cls).from_xml(xml_data, system, org, course)
@@ -248,7 +248,7 @@ class CourseDescriptor(SequenceDescriptor):
except ValueError:
system.error_tracker("Unable to decode grading policy as json")
policy = None
-
+
# cdodge: import the grading policy information that is on disk and put into the
# descriptor 'definition' bucket as a dictionary so that it is persisted in the DB
instance.definition['data']['grading_policy'] = policy
@@ -303,28 +303,28 @@ class CourseDescriptor(SequenceDescriptor):
@property
def enrollment_start(self):
return self._try_parse_time("enrollment_start")
-
+
@enrollment_start.setter
def enrollment_start(self, value):
if isinstance(value, time.struct_time):
self.metadata['enrollment_start'] = stringify_time(value)
@property
- def enrollment_end(self):
+ def enrollment_end(self):
return self._try_parse_time("enrollment_end")
-
+
@enrollment_end.setter
def enrollment_end(self, value):
if isinstance(value, time.struct_time):
self.metadata['enrollment_end'] = stringify_time(value)
-
+
@property
def grader(self):
return self._grading_policy['GRADER']
-
+
@property
def raw_grader(self):
return self._grading_policy['RAW_GRADER']
-
+
@raw_grader.setter
def raw_grader(self, value):
# NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf
@@ -334,12 +334,12 @@ class CourseDescriptor(SequenceDescriptor):
@property
def grade_cutoffs(self):
return self._grading_policy['GRADE_CUTOFFS']
-
+
@grade_cutoffs.setter
def grade_cutoffs(self, value):
self._grading_policy['GRADE_CUTOFFS'] = value
self.definition['data'].setdefault('grading_policy',{})['GRADE_CUTOFFS'] = value
-
+
@property
def lowest_passing_grade(self):
@@ -371,6 +371,19 @@ class CourseDescriptor(SequenceDescriptor):
return bool(config.get("cohorted"))
+ def cohorted_discussions(self):
+ """
+ Return the set of discussions that is cohorted. It may be the empty
+ set.
+ """
+ config = self.metadata.get("cohort-config")
+ if config is None:
+ return set()
+
+ return set(config.get("cohorted-discussions", []))
+
+
+
@property
def is_new(self):
"""
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 74f5e4c54f..89a1496eca 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -83,26 +83,6 @@ def get_opt_course_with_access(user, course_id, action):
return None
return get_course_with_access(user, course_id, action)
-
-def get_cohort_id(user, course_id):
- """
- given a course id and a user, return the id of the cohort that user is assigned to
- and if the course is not cohorted or the user is an instructor, return None
-
- """
- return 127
-
-def is_commentable_cohorted(course_id,commentable_id):
- """
- given a course and a commentable id, return whether or not this commentable is cohorted
-
- """
-
-
-def get_cohort_ids(course_id):
- """
- given a course id, return an array of all cohort ids for that course (needed for UI
- """
def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py
index c1e188ff1a..eee6c6d09d 100644
--- a/lms/djangoapps/django_comment_client/base/views.py
+++ b/lms/djangoapps/django_comment_client/base/views.py
@@ -21,7 +21,7 @@ from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
-from courseware.courses import get_cohort_id,is_commentable_cohorted
+from course_groups.cohorts import get_cohort_id, is_commentable_cohorted
from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context
diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py
index 443329ec1f..93752f6c33 100644
--- a/lms/djangoapps/django_comment_client/forum/views.py
+++ b/lms/djangoapps/django_comment_client/forum/views.py
@@ -11,7 +11,7 @@ from django.contrib.auth.models import User
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import get_course_with_access
-from courseware.courses import get_cohort_id
+from course_groups.cohorts import get_cohort_id
from courseware.access import has_access
from urllib import urlencode
From de027211586a0e4704abe6e34887059a75e44427 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 11:00:53 -0500
Subject: [PATCH 50/82] Add removing students from cohorts.
---
common/djangoapps/course_groups/views.py | 31 +++++++++++++++++
common/static/js/course_groups/cohorts.js | 34 ++++++++++++++++---
.../course_groups/cohort_management.html | 2 +-
lms/urls.py | 3 ++
4 files changed, 65 insertions(+), 5 deletions(-)
diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py
index c79839208e..e82532ae50 100644
--- a/common/djangoapps/course_groups/views.py
+++ b/common/djangoapps/course_groups/views.py
@@ -164,6 +164,37 @@ def add_users_to_cohort(request, course_id, cohort_id):
'present': present,
'unknown': unknown})
+@ensure_csrf_cookie
+def remove_user_from_cohort(request, course_id, cohort_id):
+ """
+ Expects 'username': username in POST data.
+
+ Return json dict of:
+
+ {'success': True} or
+ {'success': False,
+ 'msg': error_msg}
+ """
+ get_course_with_access(request.user, course_id, 'staff')
+
+ if request.method != "POST":
+ raise Http404("Must POST to add users to cohorts")
+
+ username = request.POST.get('username')
+ if username is None:
+ return JsonHttpReponse({'success': False,
+ 'msg': 'No username specified'})
+
+ cohort = cohorts.get_cohort_by_id(course_id, cohort_id)
+ try:
+ user = User.objects.get(username=username)
+ cohort.users.remove(user)
+ return JsonHttpReponse({'success': True})
+ except User.DoesNotExist:
+ log.debug('no user')
+ return JsonHttpReponse({'success': False,
+ 'msg': "No user '{0}'".format(username)})
+
def debug_cohort_mgmt(request, course_id):
"""
diff --git a/common/static/js/course_groups/cohorts.js b/common/static/js/course_groups/cohorts.js
index 531ce51923..ada0b16bd5 100644
--- a/common/static/js/course_groups/cohorts.js
+++ b/common/static/js/course_groups/cohorts.js
@@ -67,7 +67,8 @@ var CohortManager = (function ($) {
var detail_page_num = $$(".page_num");
var users_area = $$(".users_area");
var add_members_button = $$(".add_members");
- var op_results = $$("op_results");
+ var op_results = $$(".op_results");
+ var cohort_id = null;
var cohort_title = null;
var detail_url = null;
var page = null;
@@ -79,6 +80,7 @@ var CohortManager = (function ($) {
var el = $(this);
cohort_title = el.text();
detail_url = el.data('href');
+ cohort_id = el.data('id');
state = state_detail;
render();
}
@@ -118,12 +120,31 @@ var CohortManager = (function ($) {
// *********** Detail view methods
+ function remove_user_from_cohort(username, cohort_id, row) {
+ var delete_url = detail_url + '/delete';
+ var data = {'username': username}
+ $.post(delete_url, data).done(function() {row.remove()})
+ .fail(function(jqXHR, status, error) {
+ log_error('Error removing user ' + username +
+ ' from cohort. ' + status + ' ' + error);
+ });
+ }
+
function add_to_users_list(item) {
var tr = $('
diff --git a/lms/urls.py b/lms/urls.py
index 54f41e2c87..3353c8a6ba 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -287,6 +287,9 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/cohorts/(?P[0-9]+)/add$',
'course_groups.views.add_users_to_cohort',
name="add_to_cohort"),
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/cohorts/(?P[0-9]+)/delete$',
+ 'course_groups.views.remove_user_from_cohort',
+ name="remove_from_cohort"),
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/cohorts/debug$',
'course_groups.views.debug_cohort_mgmt',
name="debug_cohort_mgmt"),
From 3bfe9decf43b705ffcb262385d4de1971a84af7e Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 11:14:57 -0500
Subject: [PATCH 51/82] Change config keys to use underscores for consistency
with other policy keys
---
common/lib/xmodule/xmodule/course_module.py | 6 +++---
doc/xml-format.md | 5 +++--
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 5b6de04f3e..df542a3d42 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -365,7 +365,7 @@ class CourseDescriptor(SequenceDescriptor):
"""
Return whether the course is cohorted.
"""
- config = self.metadata.get("cohort-config")
+ config = self.metadata.get("cohort_config")
if config is None:
return False
@@ -376,11 +376,11 @@ class CourseDescriptor(SequenceDescriptor):
Return the set of discussions that is cohorted. It may be the empty
set.
"""
- config = self.metadata.get("cohort-config")
+ config = self.metadata.get("cohort_config")
if config is None:
return set()
- return set(config.get("cohorted-discussions", []))
+ return set(config.get("cohorted_discussions", []))
diff --git a/doc/xml-format.md b/doc/xml-format.md
index 8f9e512ac1..afa357aaef 100644
--- a/doc/xml-format.md
+++ b/doc/xml-format.md
@@ -258,9 +258,10 @@ Supported fields at the course level:
* "discussion_blackouts" -- An array of time intervals during which you want to disable a student's ability to create or edit posts in the forum. Moderators, Community TAs, and Admins are unaffected. You might use this during exam periods, but please be aware that the forum is often a very good place to catch mistakes and clarify points to students. The better long term solution would be to have better flagging/moderation mechanisms, but this is the hammer we have today. Format by example: [["2012-10-29T04:00", "2012-11-03T04:00"], ["2012-12-30T04:00", "2013-01-02T04:00"]]
* "show_calculator" (value "Yes" if desired)
* "days_early_for_beta" -- number of days (floating point ok) early that students in the beta-testers group get to see course content. Can also be specified for any other course element, and overrides values set at higher levels.
-* "cohort-config" : dictionary with keys
+* "cohort_config" : dictionary with keys
- "cohorted" : boolean. Set to true if this course uses student cohorts. If so, all inline discussions are automatically cohorted, and top-level discussion topics are configurable with an optional 'cohorted': bool parameter (with default value false).
- - ... more to come. ('auto-cohort', how to auto cohort, etc)
+ - "cohorted_discussion": list of discussions that should be cohorted.
+ - ... more to come. ('auto_cohort', how to auto cohort, etc)
* TODO: there are others
From 5fa8cf5a788a424f6614a22d568e90f02269dc90 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 11:15:14 -0500
Subject: [PATCH 52/82] Only show cohort config for cohorted courses
---
lms/templates/course_groups/cohort_management.html | 2 +-
lms/templates/courseware/instructor_dashboard.html | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/lms/templates/course_groups/cohort_management.html b/lms/templates/course_groups/cohort_management.html
index 579d6fa48d..239863beeb 100644
--- a/lms/templates/course_groups/cohort_management.html
+++ b/lms/templates/course_groups/cohort_management.html
@@ -1,7 +1,7 @@
diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html
index e9b9ae0354..7b177c6b6c 100644
--- a/lms/templates/courseware/instructor_dashboard.html
+++ b/lms/templates/courseware/instructor_dashboard.html
@@ -285,7 +285,9 @@ function goto( mode)
+ %if course.is_cohorted:
<%include file="/course_groups/cohort_management.html" />
+ %endif
%endif
%endif
From 103db8aaf35c27523a28999c76afe80ef5e39bc8 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 11:54:11 -0500
Subject: [PATCH 53/82] add conflict detection--users should be in at most one
cohort per course
---
common/djangoapps/course_groups/cohorts.py | 26 +++++++++++++++++++---
common/djangoapps/course_groups/views.py | 8 +++++++
common/static/js/course_groups/cohorts.js | 7 +++++-
3 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index 6c18491775..dfede1147a 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -117,6 +117,12 @@ def add_cohort(course_id, name):
group_type=CourseUserGroup.COHORT,
name=name)
+class CohortConflict(Exception):
+ """
+ Raised when user to be added is already in another cohort in same course.
+ """
+ pass
+
def add_user_to_cohort(cohort, username_or_email):
"""
Look up the given user, and if successful, add them to the specified cohort.
@@ -131,15 +137,29 @@ def add_user_to_cohort(cohort, username_or_email):
Raises:
User.DoesNotExist if can't find user.
- ValueError if user already present.
+ ValueError if user already present in this cohort.
+
+ CohortConflict if user already in another cohort.
"""
if '@' in username_or_email:
user = User.objects.get(email=username_or_email)
else:
user = User.objects.get(username=username_or_email)
- if cohort.users.filter(id=user.id).exists():
- raise ValueError("User {0} already present".format(user.username))
+ # If user in any cohorts in this course already, complain
+ course_cohorts = CourseUserGroup.objects.filter(
+ course_id=cohort.course_id,
+ users__id=user.id,
+ group_type=CourseUserGroup.COHORT)
+ if course_cohorts.exists():
+ if course_cohorts[0] == cohort:
+ raise ValueError("User {0} already present in cohort {1}".format(
+ user.username,
+ cohort.name))
+ else:
+ raise CohortConflict("User {0} is in another cohort {1} in course"
+ .format(user.username,
+ course_cohorts[0].name))
cohort.users.add(user)
return user
diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py
index e82532ae50..d64622dcb0 100644
--- a/common/djangoapps/course_groups/views.py
+++ b/common/djangoapps/course_groups/views.py
@@ -133,6 +133,8 @@ def add_users_to_cohort(request, course_id, cohort_id):
'added': [{'username': username,
'name': name,
'email': email}, ...],
+ 'conflict': [{'username_or_email': ...,
+ 'msg': ...}], # in another cohort
'present': [str1, str2, ...], # already there
'unknown': [str1, str2, ...]}
"""
@@ -146,6 +148,7 @@ def add_users_to_cohort(request, course_id, cohort_id):
users = request.POST.get('users', '')
added = []
present = []
+ conflict = []
unknown = []
for username_or_email in split_by_comma_and_whitespace(users):
try:
@@ -158,10 +161,15 @@ def add_users_to_cohort(request, course_id, cohort_id):
present.append(username_or_email)
except User.DoesNotExist:
unknown.append(username_or_email)
+ except cohorts.CohortConflict as err:
+ conflict.append({'username_or_email': username_or_email,
+ 'msg': str(err)})
+
return JsonHttpReponse({'success': True,
'added': added,
'present': present,
+ 'conflict': conflict,
'unknown': unknown})
@ensure_csrf_cookie
diff --git a/common/static/js/course_groups/cohorts.js b/common/static/js/course_groups/cohorts.js
index ada0b16bd5..aa3ce34b5b 100644
--- a/common/static/js/course_groups/cohorts.js
+++ b/common/static/js/course_groups/cohorts.js
@@ -166,8 +166,10 @@ var CohortManager = (function ($) {
function adder(note, color) {
return function(item) {
var li = $('')
- if (typeof item === "object") {
+ if (typeof item === "object" && item.username) {
li.text(note + ' ' + item.name + ', ' + item.username + ', ' + item.email);
+ } else if (typeof item === "object" && item.msg) {
+ li.text(note + ' ' + item.username_or_email + ', ' + item.msg);
} else {
// string
li.text(note + ' ' + item);
@@ -176,10 +178,13 @@ var CohortManager = (function ($) {
op_results.append(li);
}
}
+ op_results.empty();
if (response && response.success) {
response.added.forEach(adder("Added", "green"));
response.present.forEach(adder("Already present:", "black"));
+ response.conflict.forEach(adder("In another cohort:", "purple"));
response.unknown.forEach(adder("Unknown user:", "red"));
+ users_area.val('')
} else {
log_error(response.msg || "There was an error adding users");
}
From b02ebc46187ae5a7b615b00b13aecf68b7bf585b Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Thu, 24 Jan 2013 15:20:27 -0500
Subject: [PATCH 54/82] Add these two options in as well.
---
lms/envs/aws.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index 0c59e82023..a4aeb34a20 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -88,3 +88,9 @@ PEER_GRADING_INTERFACE = AUTH_TOKENS.get('PEER_GRADING_INTERFACE', PEER_GRADING_
PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD")
+
+# Pearson hash for import/export
+PEARSON = AUTH_TOKENS.get("PEARSON")
+
+# Datadog for events!
+DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
From ea3aebdfe56ccff14c9a493bdf8e0ae402fab2ea Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 15:46:22 -0500
Subject: [PATCH 55/82] add logging, explicit check whether course is cohorted
---
common/djangoapps/course_groups/cohorts.py | 23 +++++++++++++++++++++-
doc/xml-format.md | 2 +-
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index dfede1147a..33b683776c 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -40,10 +40,18 @@ def is_commentable_cohorted(course_id, commentable_id):
Http404 if the course doesn't exist.
"""
course = courses.get_course_by_id(course_id)
- return commentable_id in course.cohorted_discussions()
+ ans = commentable_id in course.cohorted_discussions()
+ log.debug("is_commentable_cohorted({0}, {1}) = {2}".format(course_id,
+ commentable_id,
+ ans))
+ return ans
def get_cohort(user, course_id):
+ c = _get_cohort(user, course_id)
+ log.debug("get_cohort({0}, {1}) = {2}", user, course_id, c.id)
+
+def _get_cohort(user, course_id):
"""
Given a django User and a course_id, return the user's cohort. In classes with
auto-cohorting, put the user in a cohort if they aren't in one already.
@@ -54,7 +62,20 @@ def get_cohort(user, course_id):
Returns:
A CourseUserGroup object if the User has a cohort, or None.
+
+ Raises:
+ ValueError if the course_id doesn't exist.
"""
+ # First check whether the course is cohorted (users shouldn't be in a cohort
+ # in non-cohorted courses, but settings can change after )
+ try:
+ course = courses.get_course_by_id(course_id)
+ except Http404:
+ raise ValueError("Invalid course_id")
+
+ if not course.is_cohorted:
+ return None
+
try:
group = CourseUserGroup.objects.get(course_id=course_id,
group_type=CourseUserGroup.COHORT,
diff --git a/doc/xml-format.md b/doc/xml-format.md
index afa357aaef..f4fd1054cb 100644
--- a/doc/xml-format.md
+++ b/doc/xml-format.md
@@ -260,7 +260,7 @@ Supported fields at the course level:
* "days_early_for_beta" -- number of days (floating point ok) early that students in the beta-testers group get to see course content. Can also be specified for any other course element, and overrides values set at higher levels.
* "cohort_config" : dictionary with keys
- "cohorted" : boolean. Set to true if this course uses student cohorts. If so, all inline discussions are automatically cohorted, and top-level discussion topics are configurable with an optional 'cohorted': bool parameter (with default value false).
- - "cohorted_discussion": list of discussions that should be cohorted.
+ - "cohorted_discussions": list of discussions that should be cohorted.
- ... more to come. ('auto_cohort', how to auto cohort, etc)
* TODO: there are others
From fb7614a81c0b7ebbcaf62bf84e9dc07cd95bcaef Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 15:47:41 -0500
Subject: [PATCH 56/82] oops--fix logging bug
---
common/djangoapps/course_groups/cohorts.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index 33b683776c..b7401ef8a2 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -49,7 +49,7 @@ def is_commentable_cohorted(course_id, commentable_id):
def get_cohort(user, course_id):
c = _get_cohort(user, course_id)
- log.debug("get_cohort({0}, {1}) = {2}", user, course_id, c.id)
+ log.debug("get_cohort({0}, {1}) = {2}".format(user, course_id, c.id))
def _get_cohort(user, course_id):
"""
From 872f4b6c1319c5538581507e6154b6481295e222 Mon Sep 17 00:00:00 2001
From: Brian Wilson
Date: Thu, 24 Jan 2013 16:37:02 -0500
Subject: [PATCH 57/82] fix handling of rejected accommodations
---
.../management/commands/pearson_export_ead.py | 4 ++-
.../commands/pearson_make_tc_registration.py | 25 ++++++++++---------
2 files changed, 16 insertions(+), 13 deletions(-)
diff --git a/common/djangoapps/student/management/commands/pearson_export_ead.py b/common/djangoapps/student/management/commands/pearson_export_ead.py
index 49cdc9957a..03dbce0024 100644
--- a/common/djangoapps/student/management/commands/pearson_export_ead.py
+++ b/common/djangoapps/student/management/commands/pearson_export_ead.py
@@ -7,7 +7,7 @@ from optparse import make_option
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
-from student.models import TestCenterRegistration
+from student.models import TestCenterRegistration, ACCOMMODATION_REJECTED_CODE
class Command(BaseCommand):
@@ -92,6 +92,8 @@ class Command(BaseCommand):
record["LastUpdate"] = record["LastUpdate"].strftime("%Y/%m/%d %H:%M:%S")
record["EligibilityApptDateFirst"] = record["EligibilityApptDateFirst"].strftime("%Y/%m/%d")
record["EligibilityApptDateLast"] = record["EligibilityApptDateLast"].strftime("%Y/%m/%d")
+ if record["Accommodations"] == ACCOMMODATION_REJECTED_CODE:
+ record["Accommodations"] = ""
if options['force_add']:
record['AuthorizationTransactionType'] = 'Add'
diff --git a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
index 0f28ceb283..2fcfa9ae48 100644
--- a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
+++ b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py
@@ -104,19 +104,20 @@ class Command(BaseCommand):
except TestCenterUser.DoesNotExist:
raise CommandError("User \"{}\" does not have an existing demographics record".format(username))
- # check to see if a course_id was specified, and use information from that:
+ # get an "exam" object. Check to see if a course_id was specified, and use information from that:
+ exam = None
create_dummy_exam = 'create_dummy_exam' in our_options and our_options['create_dummy_exam']
- try:
- course = course_from_id(course_id)
- if 'ignore_registration_dates' in our_options:
- examlist = [exam for exam in course.test_center_exams if exam.exam_series_code == our_options.get('exam_series_code')]
- exam = examlist[0] if len(examlist) > 0 else None
- else:
- exam = course.current_test_center_exam
- except ItemNotFoundError:
- create_dummy_exam = True
-
- if exam is None and create_dummy_exam:
+ if not create_dummy_exam:
+ try:
+ course = course_from_id(course_id)
+ if 'ignore_registration_dates' in our_options:
+ examlist = [exam for exam in course.test_center_exams if exam.exam_series_code == our_options.get('exam_series_code')]
+ exam = examlist[0] if len(examlist) > 0 else None
+ else:
+ exam = course.current_test_center_exam
+ except ItemNotFoundError:
+ pass
+ else:
# otherwise use explicit values (so we don't have to define a course):
exam_name = "Dummy Placeholder Name"
exam_info = { 'Exam_Series_Code': our_options['exam_series_code'],
From 69711a91ff7bf4854a26cddceb3154beae68a7f9 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 16:58:19 -0500
Subject: [PATCH 58/82] Fix inline discussion cohorting, debug logging
---
common/djangoapps/course_groups/cohorts.py | 19 +++++++++++++++++--
common/lib/xmodule/xmodule/course_module.py | 15 +++++++++++++--
2 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index b7401ef8a2..c09e60dd80 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -40,7 +40,18 @@ def is_commentable_cohorted(course_id, commentable_id):
Http404 if the course doesn't exist.
"""
course = courses.get_course_by_id(course_id)
- ans = commentable_id in course.cohorted_discussions()
+
+ if not course.is_cohorted:
+ # this is the easy case :)
+ ans = False
+ elif commentable_id in course.top_level_discussion_topic_ids:
+ # top level discussions have to be manually configured as cohorted
+ # (default is not)
+ ans = commentable_id in course.cohorted_discussions()
+ else:
+ # inline discussions are cohorted by default
+ ans = True
+
log.debug("is_commentable_cohorted({0}, {1}) = {2}".format(course_id,
commentable_id,
ans))
@@ -49,7 +60,11 @@ def is_commentable_cohorted(course_id, commentable_id):
def get_cohort(user, course_id):
c = _get_cohort(user, course_id)
- log.debug("get_cohort({0}, {1}) = {2}".format(user, course_id, c.id))
+ log.debug("get_cohort({0}, {1}) = {2}".format(
+ user, course_id,
+ c.id if c is not None else None))
+ return c
+
def _get_cohort(user, course_id):
"""
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index df542a3d42..d35ba3ea6a 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -371,10 +371,21 @@ class CourseDescriptor(SequenceDescriptor):
return bool(config.get("cohorted"))
+ @property
+ def top_level_discussion_topic_ids(self):
+ """
+ Return list of topic ids defined in course policy.
+ """
+ topics = self.metadata.get("discussion_topics", {})
+ return [d["id"] for d in topics.values()]
+
+
+ @property
def cohorted_discussions(self):
"""
- Return the set of discussions that is cohorted. It may be the empty
- set.
+ Return the set of discussions that is explicitly cohorted. It may be
+ the empty set. Note that all inline discussions are automatically
+ cohorted based on the course's is_cohorted setting.
"""
config = self.metadata.get("cohort_config")
if config is None:
From 22ccf9c5008f963f9dda3a6dcf3621d2033a8d3d Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 17:33:25 -0500
Subject: [PATCH 59/82] Some test cleanups
---
common/djangoapps/course_groups/cohorts.py | 1 +
common/djangoapps/course_groups/tests/tests.py | 4 +++-
common/lib/xmodule/xmodule/tests/test_import.py | 8 +++++---
3 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index c09e60dd80..1c1208b8f7 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -4,6 +4,7 @@ forums, and to the cohort admin views.
"""
from django.contrib.auth.models import User
+from django.http import Http404
import logging
from courseware import courses
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
index 676643567d..1dee9d8042 100644
--- a/common/djangoapps/course_groups/tests/tests.py
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -1,7 +1,9 @@
import django.test
from django.contrib.auth.models import User
-from course_groups.models import CourseUserGroup, get_cohort, get_course_cohorts
+from course_groups.models import CourseUserGroup
+from course_groups.cohorts import get_cohort, get_course_cohorts
+
class TestCohorts(django.test.TestCase):
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index 8a5eda3882..3c30559086 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -368,13 +368,15 @@ class ImportTestCase(unittest.TestCase):
self.assertFalse(course.is_cohorted)
# empty config -> False
- course.metadata['cohort-config'] = {}
+ course.metadata['cohort_config'] = {}
self.assertFalse(course.is_cohorted)
# false config -> False
- course.metadata['cohort-config'] = {'cohorted': False}
+ course.metadata['cohort_config'] = {'cohorted': False}
self.assertFalse(course.is_cohorted)
# and finally...
- course.metadata['cohort-config'] = {'cohorted': True}
+ course.metadata['cohort_config'] = {'cohorted': True}
self.assertTrue(course.is_cohorted)
+
+
From 0b7e7b35dc2bccf9973e8dbfd25b07b69e506c99 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 17:33:40 -0500
Subject: [PATCH 60/82] stub out django comment client test system
---
lms/djangoapps/django_comment_client/tests.py | 50 ++++++++++++++++++-
1 file changed, 48 insertions(+), 2 deletions(-)
diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py
index 2f3126be2c..8236d1b38b 100644
--- a/lms/djangoapps/django_comment_client/tests.py
+++ b/lms/djangoapps/django_comment_client/tests.py
@@ -1,11 +1,19 @@
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, Group
+from django.core.urlresolvers import reverse
from django.test import TestCase
+from django.test.client import RequestFactory
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from override_settings import override_settings
+
+import xmodule.modulestore.django
+
from student.models import CourseEnrollment, \
replicate_enrollment_save, \
replicate_enrollment_delete, \
update_user_information, \
replicate_user_save
-
+
from django.db.models.signals import m2m_changed, pre_delete, pre_save, post_delete, post_save
from django.dispatch.dispatcher import _make_id
import string
@@ -13,6 +21,44 @@ import random
from .permissions import has_permission
from .models import Role, Permission
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore import Location
+from xmodule.modulestore.xml_importer import import_from_xml
+from xmodule.modulestore.xml import XMLModuleStore
+
+
+from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
+
+@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
+class TestCohorting(PageLoader):
+ """Check that cohorting works properly"""
+
+ def setUp(self):
+ xmodule.modulestore.django._MODULESTORES = {}
+
+ # Assume courses are there
+ self.toy = modulestore().get_course("edX/toy/2012_Fall")
+
+ # Create two accounts
+ self.student = 'view@test.com'
+ self.student2 = 'view2@test.com'
+ self.password = 'foo'
+ self.create_account('u1', self.student, self.password)
+ self.create_account('u2', self.student2, self.password)
+ self.activate_user(self.student)
+ self.activate_user(self.student2)
+
+ def test_create_thread(self):
+ resp = self.client.post(reverse('create_thread'),
+ {'some': "some",
+ 'data': 'data'})
+ self.assertEqual(resp.status_code, 200)
+
+
+
+
+
+
class PermissionsTestCase(TestCase):
def random_str(self, length=15, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(length))
From d17aedf3b4413d5fcf492247cc6f3653d873345a Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 18:08:51 -0500
Subject: [PATCH 61/82] the reverse isn't working for some reason... need to
figure it out
---
lms/djangoapps/django_comment_client/tests.py | 22 ++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py
index 8236d1b38b..8b306abeed 100644
--- a/lms/djangoapps/django_comment_client/tests.py
+++ b/lms/djangoapps/django_comment_client/tests.py
@@ -4,6 +4,9 @@ from django.test import TestCase
from django.test.client import RequestFactory
from django.conf import settings
from django.core.urlresolvers import reverse
+
+from mock import Mock
+
from override_settings import override_settings
import xmodule.modulestore.django
@@ -26,6 +29,7 @@ from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml import XMLModuleStore
+import comment_client
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
@@ -49,13 +53,25 @@ class TestCohorting(PageLoader):
self.activate_user(self.student2)
def test_create_thread(self):
- resp = self.client.post(reverse('create_thread'),
+ my_save = Mock()
+ comment_client.perform_request = my_save
+
+ resp = self.client.post(
+ reverse('django_comment_client.base.views.create_thread',
+ kwargs={'course_id': 'edX/toy/2012_Fall',
+ 'commentable_id': 'General'}),
{'some': "some",
'data': 'data'})
- self.assertEqual(resp.status_code, 200)
-
+ self.assertTrue(my_save.called)
+ #self.assertEqual(resp.status_code, 200)
+ #self.assertEqual(my_save.something, "expected", "complaint if not true")
+ self.toy.metadata["cohort_config"] = {"cohorted": True}
+
+ # call the view again ...
+
+ # assert that different things happened
From 771a55a631a420de08c2c2ea701cc645783b463e Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Thu, 24 Jan 2013 18:35:34 -0500
Subject: [PATCH 62/82] turn forums on in test.py
- this may not be a good idea, but needed for testing cohorts for now...
---
lms/djangoapps/django_comment_client/tests.py | 1 -
lms/envs/test.py | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py
index 8b306abeed..413a4fd4e7 100644
--- a/lms/djangoapps/django_comment_client/tests.py
+++ b/lms/djangoapps/django_comment_client/tests.py
@@ -3,7 +3,6 @@ from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory
from django.conf import settings
-from django.core.urlresolvers import reverse
from mock import Mock
diff --git a/lms/envs/test.py b/lms/envs/test.py
index 8b546549eb..efb7312a46 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -17,7 +17,7 @@ from path import path
MITX_FEATURES['DISABLE_START_DATES'] = True
# Until we have discussion actually working in test mode, just turn it off
-MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
+MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
WIKI_ENABLED = True
From 07c1999a953be4d6094873fd53666e1b31903902 Mon Sep 17 00:00:00 2001
From: Kevin Chugh
Date: Thu, 24 Jan 2013 21:43:36 -0500
Subject: [PATCH 63/82] prep for staging
---
lms/djangoapps/django_comment_client/tests.py | 78 +++++++++----------
lms/envs/test.py | 2 +-
2 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py
index 413a4fd4e7..feda9a4676 100644
--- a/lms/djangoapps/django_comment_client/tests.py
+++ b/lms/djangoapps/django_comment_client/tests.py
@@ -32,45 +32,45 @@ import comment_client
from courseware.tests.tests import PageLoader, TEST_DATA_XML_MODULESTORE
-@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
-class TestCohorting(PageLoader):
- """Check that cohorting works properly"""
-
- def setUp(self):
- xmodule.modulestore.django._MODULESTORES = {}
-
- # Assume courses are there
- self.toy = modulestore().get_course("edX/toy/2012_Fall")
-
- # Create two accounts
- self.student = 'view@test.com'
- self.student2 = 'view2@test.com'
- self.password = 'foo'
- self.create_account('u1', self.student, self.password)
- self.create_account('u2', self.student2, self.password)
- self.activate_user(self.student)
- self.activate_user(self.student2)
-
- def test_create_thread(self):
- my_save = Mock()
- comment_client.perform_request = my_save
-
- resp = self.client.post(
- reverse('django_comment_client.base.views.create_thread',
- kwargs={'course_id': 'edX/toy/2012_Fall',
- 'commentable_id': 'General'}),
- {'some': "some",
- 'data': 'data'})
- self.assertTrue(my_save.called)
-
- #self.assertEqual(resp.status_code, 200)
- #self.assertEqual(my_save.something, "expected", "complaint if not true")
-
- self.toy.metadata["cohort_config"] = {"cohorted": True}
-
- # call the view again ...
-
- # assert that different things happened
+#@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
+#class TestCohorting(PageLoader):
+# """Check that cohorting works properly"""
+#
+# def setUp(self):
+# xmodule.modulestore.django._MODULESTORES = {}
+#
+# # Assume courses are there
+# self.toy = modulestore().get_course("edX/toy/2012_Fall")
+#
+# # Create two accounts
+# self.student = 'view@test.com'
+# self.student2 = 'view2@test.com'
+# self.password = 'foo'
+# self.create_account('u1', self.student, self.password)
+# self.create_account('u2', self.student2, self.password)
+# self.activate_user(self.student)
+# self.activate_user(self.student2)
+#
+# def test_create_thread(self):
+# my_save = Mock()
+# comment_client.perform_request = my_save
+#
+# resp = self.client.post(
+# reverse('django_comment_client.base.views.create_thread',
+# kwargs={'course_id': 'edX/toy/2012_Fall',
+# 'commentable_id': 'General'}),
+# {'some': "some",
+# 'data': 'data'})
+# self.assertTrue(my_save.called)
+#
+# #self.assertEqual(resp.status_code, 200)
+# #self.assertEqual(my_save.something, "expected", "complaint if not true")
+#
+# self.toy.metadata["cohort_config"] = {"cohorted": True}
+#
+# # call the view again ...
+#
+# # assert that different things happened
diff --git a/lms/envs/test.py b/lms/envs/test.py
index efb7312a46..a045baa669 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -17,7 +17,7 @@ from path import path
MITX_FEATURES['DISABLE_START_DATES'] = True
# Until we have discussion actually working in test mode, just turn it off
-MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
+#MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
WIKI_ENABLED = True
From a54778551edcb69c6557cd5dc2c916948e26cf8f Mon Sep 17 00:00:00 2001
From: Ashley Penney
Date: Fri, 25 Jan 2013 10:49:20 -0500
Subject: [PATCH 64/82] This needs to be a list of strings.
---
.../student/management/commands/pearson_import_conf_zip.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
index bf7c4481fd..9c3a34a90c 100644
--- a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
+++ b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py
@@ -26,7 +26,7 @@ class Command(BaseCommand):
@staticmethod
def datadog_error(string, tags):
- dog_http_api.event("Pearson Import", string, alert_type='error', tags=tags)
+ dog_http_api.event("Pearson Import", string, alert_type='error', tags=[tags])
def handle(self, *args, **kwargs):
if len(args) < 1:
From 3d303518eb3f7c327616cb99e45ea5904fe8b23a Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Fri, 25 Jan 2013 12:51:34 -0500
Subject: [PATCH 65/82] Remove old user replication code.
- we don't use it, and if we do want to change the arch, we should have a proper central user info service.
---
common/djangoapps/student/models.py | 164 ----------------------------
1 file changed, 164 deletions(-)
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 00819d7dc4..611b6effbe 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -704,167 +704,3 @@ def update_user_information(sender, instance, created, **kwargs):
log.error(unicode(e))
log.error("update user info to discussion failed for user with id: " + str(instance.id))
-
-########################## REPLICATION SIGNALS #################################
-# @receiver(post_save, sender=User)
-def replicate_user_save(sender, **kwargs):
- user_obj = kwargs['instance']
- if not should_replicate(user_obj):
- return
- for course_db_name in db_names_to_replicate_to(user_obj.id):
- replicate_user(user_obj, course_db_name)
-
-
-# @receiver(post_save, sender=CourseEnrollment)
-def replicate_enrollment_save(sender, **kwargs):
- """This is called when a Student enrolls in a course. It has to do the
- following:
-
- 1. Make sure the User is copied into the Course DB. It may already exist
- (someone deleting and re-adding a course). This has to happen first or
- the foreign key constraint breaks.
- 2. Replicate the CourseEnrollment.
- 3. Replicate the UserProfile.
- """
- if not is_portal():
- return
-
- enrollment_obj = kwargs['instance']
- log.debug("Replicating user because of new enrollment")
- for course_db_name in db_names_to_replicate_to(enrollment_obj.user.id):
- replicate_user(enrollment_obj.user, course_db_name)
-
- log.debug("Replicating enrollment because of new enrollment")
- replicate_model(CourseEnrollment.save, enrollment_obj, enrollment_obj.user_id)
-
- log.debug("Replicating user profile because of new enrollment")
- user_profile = UserProfile.objects.get(user_id=enrollment_obj.user_id)
- replicate_model(UserProfile.save, user_profile, enrollment_obj.user_id)
-
-
-# @receiver(post_delete, sender=CourseEnrollment)
-def replicate_enrollment_delete(sender, **kwargs):
- enrollment_obj = kwargs['instance']
- return replicate_model(CourseEnrollment.delete, enrollment_obj, enrollment_obj.user_id)
-
-
-# @receiver(post_save, sender=UserProfile)
-def replicate_userprofile_save(sender, **kwargs):
- """We just updated the UserProfile (say an update to the name), so push that
- change to all Course DBs that we're enrolled in."""
- user_profile_obj = kwargs['instance']
- return replicate_model(UserProfile.save, user_profile_obj, user_profile_obj.user_id)
-
-
-######### Replication functions #########
-USER_FIELDS_TO_COPY = ["id", "username", "first_name", "last_name", "email",
- "password", "is_staff", "is_active", "is_superuser",
- "last_login", "date_joined"]
-
-
-def replicate_user(portal_user, course_db_name):
- """Replicate a User to the correct Course DB. This is more complicated than
- it should be because Askbot extends the auth_user table and adds its own
- fields. So we need to only push changes to the standard fields and leave
- the rest alone so that Askbot changes at the Course DB level don't get
- overridden.
- """
- try:
- course_user = User.objects.using(course_db_name).get(id=portal_user.id)
- log.debug("User {0} found in Course DB, replicating fields to {1}"
- .format(course_user, course_db_name))
- except User.DoesNotExist:
- log.debug("User {0} not found in Course DB, creating copy in {1}"
- .format(portal_user, course_db_name))
- course_user = User()
-
- for field in USER_FIELDS_TO_COPY:
- setattr(course_user, field, getattr(portal_user, field))
-
- mark_handled(course_user)
- course_user.save(using=course_db_name)
- unmark(course_user)
-
-
-def replicate_model(model_method, instance, user_id):
- """
- model_method is the model action that we want replicated. For instance,
- UserProfile.save
- """
- if not should_replicate(instance):
- return
-
- course_db_names = db_names_to_replicate_to(user_id)
- log.debug("Replicating {0} for user {1} to DBs: {2}"
- .format(model_method, user_id, course_db_names))
-
- mark_handled(instance)
- for db_name in course_db_names:
- model_method(instance, using=db_name)
- unmark(instance)
-
-
-######### Replication Helpers #########
-
-
-def is_valid_course_id(course_id):
- """Right now, the only database that's not a course database is 'default'.
- I had nicer checking in here originally -- it would scan the courses that
- were in the system and only let you choose that. But it was annoying to run
- tests with, since we don't have course data for some for our course test
- databases. Hence the lazy version.
- """
- return course_id != 'default'
-
-
-def is_portal():
- """Are we in the portal pool? Only Portal servers are allowed to replicate
- their changes. For now, only Portal servers see multiple DBs, so we use
- that to decide."""
- return len(settings.DATABASES) > 1
-
-
-def db_names_to_replicate_to(user_id):
- """Return a list of DB names that this user_id is enrolled in."""
- return [c.course_id
- for c in CourseEnrollment.objects.filter(user_id=user_id)
- if is_valid_course_id(c.course_id)]
-
-
-def marked_handled(instance):
- """Have we marked this instance as being handled to avoid infinite loops
- caused by saving models in post_save hooks for the same models?"""
- return hasattr(instance, '_do_not_copy_to_course_db') and instance._do_not_copy_to_course_db
-
-
-def mark_handled(instance):
- """You have to mark your instance with this function or else we'll go into
- an infinite loop since we're putting listeners on Model saves/deletes and
- the act of replication requires us to call the same model method.
-
- We create a _replicated attribute to differentiate the first save of this
- model vs. the duplicate save we force on to the course database. Kind of
- a hack -- suggestions welcome.
- """
- instance._do_not_copy_to_course_db = True
-
-
-def unmark(instance):
- """If we don't unmark a model after we do replication, then consecutive
- save() calls won't be properly replicated."""
- instance._do_not_copy_to_course_db = False
-
-
-def should_replicate(instance):
- """Should this instance be replicated? We need to be a Portal server and
- the instance has to not have been marked_handled."""
- if marked_handled(instance):
- # Basically, avoid an infinite loop. You should
- log.debug("{0} should not be replicated because it's been marked"
- .format(instance))
- return False
- if not is_portal():
- log.debug("{0} should not be replicated because we're not a portal."
- .format(instance))
- return False
- return True
From 66934ce220fc91a2ed14fa7082f17e5209aed06e Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Fri, 25 Jan 2013 13:33:19 -0500
Subject: [PATCH 66/82] Address Dave's comments on
https://github.com/MITx/mitx/pull/1350
---
common/djangoapps/course_groups/cohorts.py | 6 +--
common/djangoapps/course_groups/views.py | 49 ++++++++++---------
common/djangoapps/student/models.py | 15 +++++-
common/lib/string_util.py | 11 -----
.../django_comment_client/base/views.py | 47 +++++++-----------
.../django_comment_client/forum/views.py | 20 ++++----
6 files changed, 72 insertions(+), 76 deletions(-)
delete mode 100644 common/lib/string_util.py
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index 1c1208b8f7..3fe333f9c8 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -8,6 +8,7 @@ from django.http import Http404
import logging
from courseware import courses
+from student.models import get_user_by_username_or_email
from .models import CourseUserGroup
log = logging.getLogger(__name__)
@@ -178,10 +179,7 @@ def add_user_to_cohort(cohort, username_or_email):
CohortConflict if user already in another cohort.
"""
- if '@' in username_or_email:
- user = User.objects.get(email=username_or_email)
- else:
- user = User.objects.get(username=username_or_email)
+ user = get_user_by_username_or_email(username_or_email)
# If user in any cohorts in this course already, complain
course_cohorts = CourseUserGroup.objects.filter(
diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py
index d64622dcb0..75d7f31626 100644
--- a/common/djangoapps/course_groups/views.py
+++ b/common/djangoapps/course_groups/views.py
@@ -1,5 +1,6 @@
from django_future.csrf import ensure_csrf_cookie
from django.contrib.auth.decorators import login_required
+from django.views.decorators.http import require_POST
from django.contrib.auth.models import User
from django.core.context_processors import csrf
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
@@ -8,10 +9,10 @@ from django.http import HttpResponse, HttpResponseForbidden, Http404
from django.shortcuts import redirect
import json
import logging
+import re
from courseware.courses import get_course_with_access
from mitxmako.shortcuts import render_to_response, render_to_string
-from string_util import split_by_comma_and_whitespace
from .models import CourseUserGroup
from . import cohorts
@@ -21,13 +22,20 @@ import track.views
log = logging.getLogger(__name__)
-def JsonHttpReponse(data):
+def json_http_response(data):
"""
- Return an HttpResponse with the data json-serialized and the right content type
- header. Named to look like a class.
+ Return an HttpResponse with the data json-serialized and the right content
+ type header.
"""
return HttpResponse(json.dumps(data), content_type="application/json")
+def split_by_comma_and_whitespace(s):
+ """
+ Split a string both by commas and whitespice. Returns a list.
+ """
+ return re.split(r'[\s|,|]+', s)
+
+
@ensure_csrf_cookie
def list_cohorts(request, course_id):
"""
@@ -41,11 +49,12 @@ def list_cohorts(request, course_id):
all_cohorts = [{'name': c.name, 'id': c.id}
for c in cohorts.get_course_cohorts(course_id)]
- return JsonHttpReponse({'success': True,
+ return json_http_response({'success': True,
'cohorts': all_cohorts})
@ensure_csrf_cookie
+@require_POST
def add_cohort(request, course_id):
"""
Return json of dict:
@@ -60,22 +69,18 @@ def add_cohort(request, course_id):
"""
get_course_with_access(request.user, course_id, 'staff')
-
- if request.method != "POST":
- raise Http404("Must POST to add cohorts")
-
name = request.POST.get("name")
if not name:
- return JsonHttpReponse({'success': False,
+ return json_http_response({'success': False,
'msg': "No name specified"})
try:
cohort = cohorts.add_cohort(course_id, name)
except ValueError as err:
- return JsonHttpReponse({'success': False,
+ return json_http_response({'success': False,
'msg': str(err)})
- return JsonHttpReponse({'success': 'True',
+ return json_http_response({'success': 'True',
'cohort': {
'id': cohort.id,
'name': cohort.name
@@ -98,6 +103,8 @@ def users_in_cohort(request, course_id, cohort_id):
"""
get_course_with_access(request.user, course_id, 'staff')
+ # this will error if called with a non-int cohort_id. That's ok--it
+ # shoudn't happen for valid clients.
cohort = cohorts.get_cohort_by_id(course_id, int(cohort_id))
paginator = Paginator(cohort.users.all(), 100)
@@ -118,13 +125,14 @@ def users_in_cohort(request, course_id, cohort_id):
'name': '{0} {1}'.format(u.first_name, u.last_name)}
for u in users]
- return JsonHttpReponse({'success': True,
+ return json_http_response({'success': True,
'page': page,
'num_pages': paginator.num_pages,
'users': user_info})
@ensure_csrf_cookie
+@require_POST
def add_users_to_cohort(request, course_id, cohort_id):
"""
Return json dict of:
@@ -140,9 +148,6 @@ def add_users_to_cohort(request, course_id, cohort_id):
"""
get_course_with_access(request.user, course_id, 'staff')
- if request.method != "POST":
- raise Http404("Must POST to add users to cohorts")
-
cohort = cohorts.get_cohort_by_id(course_id, cohort_id)
users = request.POST.get('users', '')
@@ -166,13 +171,14 @@ def add_users_to_cohort(request, course_id, cohort_id):
'msg': str(err)})
- return JsonHttpReponse({'success': True,
+ return json_http_response({'success': True,
'added': added,
'present': present,
'conflict': conflict,
'unknown': unknown})
@ensure_csrf_cookie
+@require_POST
def remove_user_from_cohort(request, course_id, cohort_id):
"""
Expects 'username': username in POST data.
@@ -185,22 +191,19 @@ def remove_user_from_cohort(request, course_id, cohort_id):
"""
get_course_with_access(request.user, course_id, 'staff')
- if request.method != "POST":
- raise Http404("Must POST to add users to cohorts")
-
username = request.POST.get('username')
if username is None:
- return JsonHttpReponse({'success': False,
+ return json_http_response({'success': False,
'msg': 'No username specified'})
cohort = cohorts.get_cohort_by_id(course_id, cohort_id)
try:
user = User.objects.get(username=username)
cohort.users.remove(user)
- return JsonHttpReponse({'success': True})
+ return json_http_response({'success': True})
except User.DoesNotExist:
log.debug('no user')
- return JsonHttpReponse({'success': False,
+ return json_http_response({'success': False,
'msg': "No user '{0}'".format(username)})
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 611b6effbe..ab7f435f68 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -613,7 +613,20 @@ class CourseEnrollmentAllowed(models.Model):
#cache_relation(User.profile)
-#### Helper methods for use from python manage.py shell.
+#### Helper methods for use from python manage.py shell and other classes.
+
+def get_user_by_username_or_email(username_or_email):
+ """
+ Return a User object, looking up by email if username_or_email contains a
+ '@', otherwise by username.
+
+ Raises:
+ User.DoesNotExist is lookup fails.
+ """
+ if '@' in username_or_email:
+ return User.objects.get(email=username_or_email)
+ else:
+ return User.objects.get(username=username_or_email)
def get_user(email):
diff --git a/common/lib/string_util.py b/common/lib/string_util.py
deleted file mode 100644
index 0db385f2d6..0000000000
--- a/common/lib/string_util.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import itertools
-
-def split_by_comma_and_whitespace(s):
- """
- Split a string both by on commas and whitespice.
- """
- # Note: split() with no args removes empty strings from output
- lists = [x.split() for x in s.split(',')]
- # return all of them
- return itertools.chain(*lists)
-
diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py
index eee6c6d09d..777c7bafce 100644
--- a/lms/djangoapps/django_comment_client/base/views.py
+++ b/lms/djangoapps/django_comment_client/base/views.py
@@ -28,6 +28,8 @@ from django_comment_client.utils import JsonResponse, JsonError, extract, get_co
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
from django_comment_client.models import Role
+log = logging.getLogger(__name__)
+
def permitted(fn):
@functools.wraps(fn)
def wrapper(request, *args, **kwargs):
@@ -59,17 +61,12 @@ def ajax_content_response(request, course_id, content, template_name):
'annotated_content_info': annotated_content_info,
})
-
-
-def is_moderator(user, course_id):
- cached_has_permission(user, "see_all_cohorts", course_id)
-
+
@require_POST
@login_required
@permitted
def create_thread(request, course_id, commentable_id):
- print "\n\n\n\n\n*******************"
- print commentable_id
+ log.debug("Creating new thread in %r, id %r", course_id, commentable_id)
course = get_course_with_access(request.user, course_id, 'load')
post = request.POST
@@ -91,29 +88,23 @@ def create_thread(request, course_id, commentable_id):
'course_id' : course_id,
'user_id' : request.user.id,
})
-
-
- #now cohort the thread if the commentable is cohorted
- #if the group id came in from the form, set it there, otherwise,
- #see if the user and the commentable are cohorted
- if is_commentable_cohorted(course_id,commentable_id):
- if 'group_id' in post: #if a group id was submitted in the form
- posted_group_id = post['group_id']
- else:
- post_group_id = None
-
- user_group_id = get_cohort_id(request.user, course_id)
- if is_moderator(request.user,course_id):
- if post_group_id is None:
- group_id = user_group_id
+
+ # Cohort the thread if the commentable is cohorted.
+ if is_commentable_cohorted(course_id, commentable_id):
+ user_group_id = get_cohort_id(request.user, course_id)
+ # TODO (vshnayder): once we have more than just cohorts, we'll want to
+ # change this to a single get_group_for_user_and_commentable function
+ # that can do different things depending on the commentable_id
+ if cached_has_permission(request.user, "see_all_cohorts", course_id):
+ # admins can optionally choose what group to post as
+ group_id = post.get('group_id', user_group_id)
else:
- group_id = post_group_id
- else:
- group_id = user_group_id
-
- thread.update_attributes(**{'group_id' :group_id})
-
+ # regular users always post with their own id.
+ group_id = user_group_id
+
+ thread.update_attributes(group_id=group_id)
+
thread.save()
if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user)
diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py
index 93752f6c33..6a182665a8 100644
--- a/lms/djangoapps/django_comment_client/forum/views.py
+++ b/lms/djangoapps/django_comment_client/forum/views.py
@@ -17,7 +17,8 @@ from courseware.access import has_access
from urllib import urlencode
from operator import methodcaller
from django_comment_client.permissions import check_permissions_by_view
-from django_comment_client.utils import merge_dict, extract, strip_none, strip_blank, get_courseware_context
+from django_comment_client.utils import (merge_dict, extract, strip_none,
+ strip_blank, get_courseware_context)
import django_comment_client.utils as utils
import comment_client as cc
@@ -58,16 +59,17 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
user.default_sort_key = request.GET.get('sort_key')
user.save()
-
+
#if the course-user is cohorted, then add the group id
- group_id = get_cohort_id(user,course_id);
+ group_id = get_cohort_id(user,course_id)
if group_id:
- default_query_params["group_id"] = group_id;
- print("\n\n\n\n\n****************GROUP ID IS ")
- print group_id
-
+ default_query_params["group_id"] = group_id
+
query_params = merge_dict(default_query_params,
- strip_none(extract(request.GET, ['page', 'sort_key', 'sort_order', 'text', 'tags', 'commentable_ids'])))
+ strip_none(extract(request.GET,
+ ['page', 'sort_key',
+ 'sort_order', 'text',
+ 'tags', 'commentable_ids'])))
threads, page, num_pages = cc.Thread.search(query_params)
@@ -226,7 +228,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
# course_id,
#)
-
+
annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
context = {
From 76c4730532522e701b70aef9880bf3b24b56fbd7 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Fri, 25 Jan 2013 13:33:25 -0500
Subject: [PATCH 67/82] whitespace
---
common/djangoapps/student/models.py | 142 ++++++++++++++--------------
1 file changed, 71 insertions(+), 71 deletions(-)
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index ab7f435f68..01a06b8bcc 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -121,8 +121,8 @@ class TestCenterUser(models.Model):
The field names and lengths are modeled on the conventions and constraints
of Pearson's data import system, including oddities such as suffix having
a limit of 255 while last_name only gets 50.
-
- Also storing here the confirmation information received from Pearson (if any)
+
+ Also storing here the confirmation information received from Pearson (if any)
as to the success or failure of the upload. (VCDC file)
"""
# Our own record keeping...
@@ -172,7 +172,7 @@ class TestCenterUser(models.Model):
uploaded_at = models.DateTimeField(null=True, blank=True, db_index=True)
# confirmation back from the test center, as well as timestamps
- # on when they processed the request, and when we received
+ # on when they processed the request, and when we received
# confirmation back.
processed_at = models.DateTimeField(null=True, db_index=True)
upload_status = models.CharField(max_length=20, blank=True, db_index=True) # 'Error' or 'Accepted'
@@ -186,52 +186,52 @@ class TestCenterUser(models.Model):
@property
def needs_uploading(self):
return self.uploaded_at is None or self.uploaded_at < self.user_updated_at
-
+
@staticmethod
def user_provided_fields():
- return [ 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
- 'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
+ return [ 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
+ 'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name']
-
+
@property
def email(self):
return self.user.email
-
+
def needs_update(self, fields):
for fieldname in TestCenterUser.user_provided_fields():
if fieldname in fields and getattr(self, fieldname) != fields[fieldname]:
return True
-
- return False
-
+
+ return False
+
@staticmethod
def _generate_edx_id(prefix):
NUM_DIGITS = 12
return u"{}{:012}".format(prefix, randint(1, 10**NUM_DIGITS-1))
-
+
@staticmethod
def _generate_candidate_id():
return TestCenterUser._generate_edx_id("edX")
-
+
@classmethod
def create(cls, user):
testcenter_user = cls(user=user)
- # testcenter_user.candidate_id remains unset
+ # testcenter_user.candidate_id remains unset
# assign an ID of our own:
cand_id = cls._generate_candidate_id()
while TestCenterUser.objects.filter(client_candidate_id=cand_id).exists():
cand_id = cls._generate_candidate_id()
- testcenter_user.client_candidate_id = cand_id
+ testcenter_user.client_candidate_id = cand_id
return testcenter_user
@property
def is_accepted(self):
return self.upload_status == TEST_CENTER_STATUS_ACCEPTED
-
+
@property
def is_rejected(self):
return self.upload_status == TEST_CENTER_STATUS_ERROR
-
+
@property
def is_pending(self):
return not self.is_accepted and not self.is_rejected
@@ -239,26 +239,26 @@ class TestCenterUser(models.Model):
class TestCenterUserForm(ModelForm):
class Meta:
model = TestCenterUser
- fields = ( 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
- 'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
+ fields = ( 'first_name', 'middle_name', 'last_name', 'suffix', 'salutation',
+ 'address_1', 'address_2', 'address_3', 'city', 'state', 'postal_code', 'country',
'phone', 'extension', 'phone_country_code', 'fax', 'fax_country_code', 'company_name')
-
+
def update_and_save(self):
new_user = self.save(commit=False)
# create additional values here:
new_user.user_updated_at = datetime.utcnow()
new_user.upload_status = ''
new_user.save()
- log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.user.username))
-
+ log.info("Updated demographic information for user's test center exam registration: username \"{}\" ".format(new_user.user.username))
+
# add validation:
-
+
def clean_country(self):
code = self.cleaned_data['country']
if code and len(code) != 3:
raise forms.ValidationError(u'Must be three characters (ISO 3166-1): e.g. USA, CAN, MNG')
return code
-
+
def clean(self):
def _can_encode_as_latin(fieldvalue):
try:
@@ -266,40 +266,40 @@ class TestCenterUserForm(ModelForm):
except UnicodeEncodeError:
return False
return True
-
+
cleaned_data = super(TestCenterUserForm, self).clean()
-
+
# check for interactions between fields:
if 'country' in cleaned_data:
country = cleaned_data.get('country')
if country == 'USA' or country == 'CAN':
if 'state' in cleaned_data and len(cleaned_data['state']) == 0:
- self._errors['state'] = self.error_class([u'Required if country is USA or CAN.'])
+ self._errors['state'] = self.error_class([u'Required if country is USA or CAN.'])
del cleaned_data['state']
if 'postal_code' in cleaned_data and len(cleaned_data['postal_code']) == 0:
- self._errors['postal_code'] = self.error_class([u'Required if country is USA or CAN.'])
+ self._errors['postal_code'] = self.error_class([u'Required if country is USA or CAN.'])
del cleaned_data['postal_code']
-
+
if 'fax' in cleaned_data and len(cleaned_data['fax']) > 0 and 'fax_country_code' in cleaned_data and len(cleaned_data['fax_country_code']) == 0:
- self._errors['fax_country_code'] = self.error_class([u'Required if fax is specified.'])
+ self._errors['fax_country_code'] = self.error_class([u'Required if fax is specified.'])
del cleaned_data['fax_country_code']
# check encoding for all fields:
cleaned_data_fields = [fieldname for fieldname in cleaned_data]
for fieldname in cleaned_data_fields:
if not _can_encode_as_latin(cleaned_data[fieldname]):
- self._errors[fieldname] = self.error_class([u'Must only use characters in Latin-1 (iso-8859-1) encoding'])
+ self._errors[fieldname] = self.error_class([u'Must only use characters in Latin-1 (iso-8859-1) encoding'])
del cleaned_data[fieldname]
# Always return the full collection of cleaned data.
return cleaned_data
-
-# our own code to indicate that a request has been rejected.
-ACCOMMODATION_REJECTED_CODE = 'NONE'
-
+
+# our own code to indicate that a request has been rejected.
+ACCOMMODATION_REJECTED_CODE = 'NONE'
+
ACCOMMODATION_CODES = (
- (ACCOMMODATION_REJECTED_CODE, 'No Accommodation Granted'),
+ (ACCOMMODATION_REJECTED_CODE, 'No Accommodation Granted'),
('EQPMNT', 'Equipment'),
('ET12ET', 'Extra Time - 1/2 Exam Time'),
('ET30MN', 'Extra Time - 30 Minutes'),
@@ -309,11 +309,11 @@ ACCOMMODATION_CODES = (
('SRRERC', 'Separate Room and Reader/Recorder'),
('SRRECR', 'Separate Room and Recorder'),
('SRSEAN', 'Separate Room and Service Animal'),
- ('SRSGNR', 'Separate Room and Sign Language Interpreter'),
+ ('SRSGNR', 'Separate Room and Sign Language Interpreter'),
)
ACCOMMODATION_CODE_DICT = { code : name for (code, name) in ACCOMMODATION_CODES }
-
+
class TestCenterRegistration(models.Model):
"""
This is our representation of a user's registration for in-person testing,
@@ -328,20 +328,20 @@ class TestCenterRegistration(models.Model):
of Pearson's data import system.
"""
# to find an exam registration, we key off of the user and course_id.
- # If multiple exams per course are possible, we would also need to add the
+ # If multiple exams per course are possible, we would also need to add the
# exam_series_code.
testcenter_user = models.ForeignKey(TestCenterUser, default=None)
course_id = models.CharField(max_length=128, db_index=True)
-
+
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, db_index=True)
# user_updated_at happens only when the user makes a change to their data,
# and is something Pearson needs to know to manage updates. Unlike
# updated_at, this will not get incremented when we do a batch data import.
- # The appointment dates, the exam count, and the accommodation codes can be updated,
+ # The appointment dates, the exam count, and the accommodation codes can be updated,
# but hopefully this won't happen often.
user_updated_at = models.DateTimeField(db_index=True)
- # "client_authorization_id" is our unique identifier for the authorization.
+ # "client_authorization_id" is our unique identifier for the authorization.
# This must be present for an update or delete to be sent to Pearson.
client_authorization_id = models.CharField(max_length=20, unique=True, db_index=True)
@@ -351,10 +351,10 @@ class TestCenterRegistration(models.Model):
eligibility_appointment_date_last = models.DateField(db_index=True)
# this is really a list of codes, using an '*' as a delimiter.
- # So it's not a choice list. We use the special value of ACCOMMODATION_REJECTED_CODE
+ # So it's not a choice list. We use the special value of ACCOMMODATION_REJECTED_CODE
# to indicate the rejection of an accommodation request.
accommodation_code = models.CharField(max_length=64, blank=True)
-
+
# store the original text of the accommodation request.
accommodation_request = models.CharField(max_length=1024, blank=True, db_index=True)
@@ -362,7 +362,7 @@ class TestCenterRegistration(models.Model):
uploaded_at = models.DateTimeField(null=True, db_index=True)
# confirmation back from the test center, as well as timestamps
- # on when they processed the request, and when we received
+ # on when they processed the request, and when we received
# confirmation back.
processed_at = models.DateTimeField(null=True, db_index=True)
upload_status = models.CharField(max_length=20, blank=True, db_index=True) # 'Error' or 'Accepted'
@@ -372,11 +372,11 @@ class TestCenterRegistration(models.Model):
# (However, it may never be set if we are always initiating such candidate creation.)
authorization_id = models.IntegerField(null=True, db_index=True)
confirmed_at = models.DateTimeField(null=True, db_index=True)
-
+
@property
def candidate_id(self):
return self.testcenter_user.candidate_id
-
+
@property
def client_candidate_id(self):
return self.testcenter_user.client_candidate_id
@@ -389,20 +389,20 @@ class TestCenterRegistration(models.Model):
return 'Add'
else:
# TODO: decide what to send when we have uploaded an initial version,
- # but have not received confirmation back from that upload. If the
+ # but have not received confirmation back from that upload. If the
# registration here has been changed, then we don't know if this changed
- # registration should be submitted as an 'add' or an 'update'.
+ # registration should be submitted as an 'add' or an 'update'.
#
- # If the first registration were lost or in error (e.g. bad code),
+ # If the first registration were lost or in error (e.g. bad code),
# the second should be an "Add". If the first were processed successfully,
# then the second should be an "Update". We just don't know....
return 'Update'
-
+
@property
def exam_authorization_count(self):
# TODO: figure out if this should really go in the database (with a default value).
return 1
-
+
@classmethod
def create(cls, testcenter_user, exam, accommodation_request):
registration = cls(testcenter_user = testcenter_user)
@@ -418,7 +418,7 @@ class TestCenterRegistration(models.Model):
@staticmethod
def _generate_authorization_id():
return TestCenterUser._generate_edx_id("edXexam")
-
+
@staticmethod
def _create_client_authorization_id():
"""
@@ -430,8 +430,8 @@ class TestCenterRegistration(models.Model):
while TestCenterRegistration.objects.filter(client_authorization_id=auth_id).exists():
auth_id = TestCenterRegistration._generate_authorization_id()
return auth_id
-
- # methods for providing registration status details on registration page:
+
+ # methods for providing registration status details on registration page:
@property
def demographics_is_accepted(self):
return self.testcenter_user.is_accepted
@@ -439,7 +439,7 @@ class TestCenterRegistration(models.Model):
@property
def demographics_is_rejected(self):
return self.testcenter_user.is_rejected
-
+
@property
def demographics_is_pending(self):
return self.testcenter_user.is_pending
@@ -451,7 +451,7 @@ class TestCenterRegistration(models.Model):
@property
def accommodation_is_rejected(self):
return len(self.accommodation_request) > 0 and self.accommodation_code == ACCOMMODATION_REJECTED_CODE
-
+
@property
def accommodation_is_pending(self):
return len(self.accommodation_request) > 0 and len(self.accommodation_code) == 0
@@ -463,20 +463,20 @@ class TestCenterRegistration(models.Model):
@property
def registration_is_accepted(self):
return self.upload_status == TEST_CENTER_STATUS_ACCEPTED
-
+
@property
def registration_is_rejected(self):
return self.upload_status == TEST_CENTER_STATUS_ERROR
-
+
@property
def registration_is_pending(self):
return not self.registration_is_accepted and not self.registration_is_rejected
- # methods for providing registration status summary on dashboard page:
+ # methods for providing registration status summary on dashboard page:
@property
def is_accepted(self):
return self.registration_is_accepted and self.demographics_is_accepted
-
+
@property
def is_rejected(self):
return self.registration_is_rejected or self.demographics_is_rejected
@@ -484,17 +484,17 @@ class TestCenterRegistration(models.Model):
@property
def is_pending(self):
return not self.is_accepted and not self.is_rejected
-
+
def get_accommodation_codes(self):
return self.accommodation_code.split('*')
def get_accommodation_names(self):
- return [ ACCOMMODATION_CODE_DICT.get(code, "Unknown code " + code) for code in self.get_accommodation_codes() ]
+ return [ ACCOMMODATION_CODE_DICT.get(code, "Unknown code " + code) for code in self.get_accommodation_codes() ]
@property
def registration_signup_url(self):
return settings.PEARSONVUE_SIGNINPAGE_URL
-
+
class TestCenterRegistrationForm(ModelForm):
class Meta:
model = TestCenterRegistration
@@ -505,33 +505,33 @@ class TestCenterRegistrationForm(ModelForm):
if code and len(code) > 0:
return code.strip()
return code
-
+
def update_and_save(self):
registration = self.save(commit=False)
# create additional values here:
registration.user_updated_at = datetime.utcnow()
registration.upload_status = ''
registration.save()
- log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code))
+ log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code))
# TODO: add validation code for values added to accommodation_code field.
-
-
-
+
+
+
def get_testcenter_registration(user, course_id, exam_series_code):
try:
tcu = TestCenterUser.objects.get(user=user)
except TestCenterUser.DoesNotExist:
return []
return TestCenterRegistration.objects.filter(testcenter_user=tcu, course_id=course_id, exam_series_code=exam_series_code)
-
+
def unique_id_for_user(user):
"""
Return a unique id for a user, suitable for inserting into
e.g. personalized survey links.
"""
# include the secret key as a salt, and to make the ids unique across
- # different LMS installs.
+ # different LMS installs.
h = hashlib.md5()
h.update(settings.SECRET_KEY)
h.update(str(user.id))
From d453a6136b107bb4f00085e2d034b2e4d30bb914 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Fri, 25 Jan 2013 13:42:04 -0500
Subject: [PATCH 68/82] fix regexp
---
common/djangoapps/course_groups/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/djangoapps/course_groups/views.py b/common/djangoapps/course_groups/views.py
index 75d7f31626..d591c44356 100644
--- a/common/djangoapps/course_groups/views.py
+++ b/common/djangoapps/course_groups/views.py
@@ -33,7 +33,7 @@ def split_by_comma_and_whitespace(s):
"""
Split a string both by commas and whitespice. Returns a list.
"""
- return re.split(r'[\s|,|]+', s)
+ return re.split(r'[\s,]+', s)
@ensure_csrf_cookie
From 00dd3ad9f8b5401d4fd2b84a410dcec43240e6c5 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Fri, 25 Jan 2013 15:40:13 -0500
Subject: [PATCH 69/82] Fix some of the test bugs. Still more to fix.
---
common/djangoapps/student/tests.py | 188 +-----------------
lms/djangoapps/django_comment_client/tests.py | 6 +-
lms/djangoapps/instructor/views.py | 4 +-
3 files changed, 6 insertions(+), 192 deletions(-)
diff --git a/common/djangoapps/student/tests.py b/common/djangoapps/student/tests.py
index 4c7c9e2592..8ce407bcd1 100644
--- a/common/djangoapps/student/tests.py
+++ b/common/djangoapps/student/tests.py
@@ -5,16 +5,11 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
import logging
-from datetime import datetime
-from hashlib import sha1
from django.test import TestCase
-from mock import patch, Mock
-from nose.plugins.skip import SkipTest
+from mock import Mock
-from .models import (User, UserProfile, CourseEnrollment,
- replicate_user, USER_FIELDS_TO_COPY,
- unique_id_for_user)
+from .models import unique_id_for_user
from .views import process_survey_link, _cert_info
COURSE_1 = 'edX/toy/2012_Fall'
@@ -22,185 +17,6 @@ COURSE_2 = 'edx/full/6.002_Spring_2012'
log = logging.getLogger(__name__)
-class ReplicationTest(TestCase):
-
- multi_db = True
-
- def test_user_replication(self):
- """Test basic user replication."""
- raise SkipTest()
- portal_user = User.objects.create_user('rusty', 'rusty@edx.org', 'fakepass')
- portal_user.first_name='Rusty'
- portal_user.last_name='Skids'
- portal_user.is_staff=True
- portal_user.is_active=True
- portal_user.is_superuser=True
- portal_user.last_login=datetime(2012, 1, 1)
- portal_user.date_joined=datetime(2011, 1, 1)
- # This is an Askbot field and will break if askbot is not included
-
- if hasattr(portal_user, 'seen_response_count'):
- portal_user.seen_response_count = 10
-
- portal_user.save(using='default')
-
- # We replicate this user to Course 1, then pull the same user and verify
- # that the fields copied over properly.
- replicate_user(portal_user, COURSE_1)
- course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
-
- # Make sure the fields we care about got copied over for this user.
- for field in USER_FIELDS_TO_COPY:
- self.assertEqual(getattr(portal_user, field),
- getattr(course_user, field),
- "{0} not copied from {1} to {2}".format(
- field, portal_user, course_user
- ))
-
- # This hasattr lameness is here because we don't want this test to be
- # triggered when we're being run by CMS tests (Askbot doesn't exist
- # there, so the test will fail).
- #
- # seen_response_count isn't a field we care about, so it shouldn't have
- # been copied over.
- if hasattr(portal_user, 'seen_response_count'):
- portal_user.seen_response_count = 20
- replicate_user(portal_user, COURSE_1)
- course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
- self.assertEqual(portal_user.seen_response_count, 20)
- self.assertEqual(course_user.seen_response_count, 0)
-
- # Another replication should work for an email change however, since
- # it's a field we care about.
- portal_user.email = "clyde@edx.org"
- replicate_user(portal_user, COURSE_1)
- course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
- self.assertEqual(portal_user.email, course_user.email)
-
- # During this entire time, the user data should never have made it over
- # to COURSE_2
- self.assertRaises(User.DoesNotExist,
- User.objects.using(COURSE_2).get,
- id=portal_user.id)
-
-
- def test_enrollment_for_existing_user_info(self):
- """Test the effect of Enrolling in a class if you've already got user
- data to be copied over."""
- raise SkipTest()
- # Create our User
- portal_user = User.objects.create_user('jack', 'jack@edx.org', 'fakepass')
- portal_user.first_name = "Jack"
- portal_user.save()
-
- # Set up our UserProfile info
- portal_user_profile = UserProfile.objects.create(
- user=portal_user,
- name="Jack Foo",
- level_of_education=None,
- gender='m',
- mailing_address=None,
- goals="World domination",
- )
- portal_user_profile.save()
-
- # Now let's see if creating a CourseEnrollment copies all the relevant
- # data.
- portal_enrollment = CourseEnrollment.objects.create(user=portal_user,
- course_id=COURSE_1)
- portal_enrollment.save()
-
- # Grab all the copies we expect
- course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
- self.assertEquals(portal_user, course_user)
- self.assertRaises(User.DoesNotExist,
- User.objects.using(COURSE_2).get,
- id=portal_user.id)
-
- course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id)
- self.assertEquals(portal_enrollment, course_enrollment)
- self.assertRaises(CourseEnrollment.DoesNotExist,
- CourseEnrollment.objects.using(COURSE_2).get,
- id=portal_enrollment.id)
-
- course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id)
- self.assertEquals(portal_user_profile, course_user_profile)
- self.assertRaises(UserProfile.DoesNotExist,
- UserProfile.objects.using(COURSE_2).get,
- id=portal_user_profile.id)
-
- log.debug("Make sure our seen_response_count is not replicated.")
- if hasattr(portal_user, 'seen_response_count'):
- portal_user.seen_response_count = 200
- course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
- self.assertEqual(portal_user.seen_response_count, 200)
- self.assertEqual(course_user.seen_response_count, 0)
- portal_user.save()
-
- course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
- self.assertEqual(portal_user.seen_response_count, 200)
- self.assertEqual(course_user.seen_response_count, 0)
-
- portal_user.email = 'jim@edx.org'
- portal_user.save()
- course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
- self.assertEqual(portal_user.email, 'jim@edx.org')
- self.assertEqual(course_user.email, 'jim@edx.org')
-
-
-
- def test_enrollment_for_user_info_after_enrollment(self):
- """Test the effect of modifying User data after you've enrolled."""
- raise SkipTest()
-
- # Create our User
- portal_user = User.objects.create_user('patty', 'patty@edx.org', 'fakepass')
- portal_user.first_name = "Patty"
- portal_user.save()
-
- # Set up our UserProfile info
- portal_user_profile = UserProfile.objects.create(
- user=portal_user,
- name="Patty Foo",
- level_of_education=None,
- gender='f',
- mailing_address=None,
- goals="World peace",
- )
- portal_user_profile.save()
-
- # Now let's see if creating a CourseEnrollment copies all the relevant
- # data when things are saved.
- portal_enrollment = CourseEnrollment.objects.create(user=portal_user,
- course_id=COURSE_1)
- portal_enrollment.save()
-
- portal_user.last_name = "Bar"
- portal_user.save()
- portal_user_profile.gender = 'm'
- portal_user_profile.save()
-
- # Grab all the copies we expect, and make sure it doesn't end up in
- # places we don't expect.
- course_user = User.objects.using(COURSE_1).get(id=portal_user.id)
- self.assertEquals(portal_user, course_user)
- self.assertRaises(User.DoesNotExist,
- User.objects.using(COURSE_2).get,
- id=portal_user.id)
-
- course_enrollment = CourseEnrollment.objects.using(COURSE_1).get(id=portal_enrollment.id)
- self.assertEquals(portal_enrollment, course_enrollment)
- self.assertRaises(CourseEnrollment.DoesNotExist,
- CourseEnrollment.objects.using(COURSE_2).get,
- id=portal_enrollment.id)
-
- course_user_profile = UserProfile.objects.using(COURSE_1).get(id=portal_user_profile.id)
- self.assertEquals(portal_user_profile, course_user_profile)
- self.assertRaises(UserProfile.DoesNotExist,
- UserProfile.objects.using(COURSE_2).get,
- id=portal_user_profile.id)
-
-
class CourseEndingTest(TestCase):
"""Test things related to course endings: certificates, surveys, etc"""
diff --git a/lms/djangoapps/django_comment_client/tests.py b/lms/djangoapps/django_comment_client/tests.py
index feda9a4676..ac059a1e3f 100644
--- a/lms/djangoapps/django_comment_client/tests.py
+++ b/lms/djangoapps/django_comment_client/tests.py
@@ -10,11 +10,7 @@ from override_settings import override_settings
import xmodule.modulestore.django
-from student.models import CourseEnrollment, \
- replicate_enrollment_save, \
- replicate_enrollment_delete, \
- update_user_information, \
- replicate_user_save
+from student.models import CourseEnrollment
from django.db.models.signals import m2m_changed, pre_delete, pre_save, post_delete, post_save
from django.dispatch.dispatcher import _make_id
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 1b51698834..f0a0ef324a 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -31,7 +31,6 @@ from django_comment_client.models import (Role,
FORUM_ROLE_COMMUNITY_TA)
from django_comment_client.utils import has_forum_access
from psychometrics import psychoanalyze
-from string_util import split_by_comma_and_whitespace
from student.models import CourseEnrollment, CourseEnrollmentAllowed
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location
@@ -50,6 +49,9 @@ template_imports = {'urllib': urllib}
FORUM_ROLE_ADD = 'add'
FORUM_ROLE_REMOVE = 'remove'
+def split_by_comma_and_whitespace(s):
+ return re.split(r'[\s,]', s)
+
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard(request, course_id):
From 87e92644b638fbb95f99f13f70d5e02da3cde92d Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Fri, 25 Jan 2013 16:13:55 -0500
Subject: [PATCH 70/82] Make all tests pass
caveat: with a bit of hackery. Need to do better testing once things are live.
---
cms/envs/common.py | 4 +-
.../djangoapps/course_groups/tests/tests.py | 42 ++++++++++++++++---
.../lib/xmodule/xmodule/tests/test_import.py | 5 ++-
3 files changed, 41 insertions(+), 10 deletions(-)
diff --git a/cms/envs/common.py b/cms/envs/common.py
index c047d689ce..46e2d0ef33 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -34,7 +34,7 @@ MITX_FEATURES = {
'GITHUB_PUSH': False,
'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES' : False,
- 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
+ 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
}
ENABLE_JASMINE = False
@@ -281,7 +281,7 @@ INSTALLED_APPS = (
'contentstore',
'auth',
'student', # misleading name due to sharing with lms
-
+ 'course_groups', # not used in cms (yet), but tests run
# For asset pipelining
'pipeline',
'staticfiles',
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
index 1dee9d8042..509797b6f6 100644
--- a/common/djangoapps/course_groups/tests/tests.py
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -1,15 +1,45 @@
+from nose import SkipTest
import django.test
from django.contrib.auth.models import User
+from django.conf import settings
+
+from override_settings import override_settings
+
from course_groups.models import CourseUserGroup
from course_groups.cohorts import get_cohort, get_course_cohorts
+from xmodule.tests.test_import import BaseCourseTestCase
+from xmodule.modulestore.django import modulestore
+def xml_store_config(data_dir):
+ return {
+ 'default': {
+ 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
+ 'OPTIONS': {
+ 'data_dir': data_dir,
+ 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
+ }
+ }
+}
+
+TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
+TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
+
+@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCohorts(django.test.TestCase):
def test_get_cohort(self):
- course_id = "a/b/c"
- cohort = CourseUserGroup.objects.create(name="TestCohort", course_id=course_id,
+ # Need to fix this, but after we're testing on staging. (Looks like
+ # problem is that when get_cohort internally tries to look up the
+ # course.id, it fails, even though we loaded it through the modulestore.
+
+ # Proper fix: give all tests a standard modulestore that uses the test
+ # dir.
+ raise SkipTest()
+ course = modulestore().get_course("edX/toy/2012_Fall")
+ cohort = CourseUserGroup.objects.create(name="TestCohort",
+ course_id=course.id,
group_type=CourseUserGroup.COHORT)
user = User.objects.create(username="test", email="a@b.com")
@@ -17,16 +47,16 @@ class TestCohorts(django.test.TestCase):
cohort.users.add(user)
- got = get_cohort(user, course_id)
+ got = get_cohort(user, course.id)
self.assertEquals(got.id, cohort.id, "Should find the right cohort")
- got = get_cohort(other_user, course_id)
+ got = get_cohort(other_user, course.id)
self.assertEquals(got, None, "other_user shouldn't have a cohort")
def test_get_course_cohorts(self):
- course1_id = "a/b/c"
- course2_id = "e/f/g"
+ course1_id = 'a/b/c'
+ course2_id = 'e/f/g'
# add some cohorts to course 1
cohort = CourseUserGroup.objects.create(name="TestCohort",
diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py
index 3c30559086..7cd91223e3 100644
--- a/common/lib/xmodule/xmodule/tests/test_import.py
+++ b/common/lib/xmodule/xmodule/tests/test_import.py
@@ -45,7 +45,7 @@ class DummySystem(ImportSystem):
raise Exception("Shouldn't be called")
-class ImportTestCase(unittest.TestCase):
+class BaseCourseTestCase(unittest.TestCase):
'''Make sure module imports work properly, including for malformed inputs'''
@staticmethod
def get_system(load_error_modules=True):
@@ -61,6 +61,7 @@ class ImportTestCase(unittest.TestCase):
self.assertEquals(len(courses), 1)
return courses[0]
+class ImportTestCase(BaseCourseTestCase):
def test_fallback(self):
'''Check that malformed xml loads as an ErrorDescriptor.'''
@@ -379,4 +380,4 @@ class ImportTestCase(unittest.TestCase):
course.metadata['cohort_config'] = {'cohorted': True}
self.assertTrue(course.is_cohorted)
-
+
From c56982e6f0a9418c5a42404cba04d94174f0a565 Mon Sep 17 00:00:00 2001
From: ichuang
Date: Fri, 25 Jan 2013 16:45:16 -0500
Subject: [PATCH 71/82] fix bug in video player:if from=xx:xx:xx is used then
bkgrnd flashes
---
.../lib/xmodule/xmodule/js/src/video/display/video_player.coffee | 1 +
1 file changed, 1 insertion(+)
diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee
index 93f90d9248..22308a5568 100644
--- a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee
+++ b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee
@@ -45,6 +45,7 @@ class @VideoPlayer extends Subview
modestbranding: 1
if @video.start
@playerVars.start = @video.start
+ @playerVars.wmode = 'window'
if @video.end
# work in AS3, not HMLT5. but iframe use AS3
@playerVars.end = @video.end
From 109689fe9a4aaf5750e0112103494636bc62ace7 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Sat, 26 Jan 2013 00:01:47 -0500
Subject: [PATCH 72/82] Fix versions in gemfile
---
Gemfile | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/Gemfile b/Gemfile
index 0fe7df217d..b95f3b5d1a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,7 +1,7 @@
source :rubygems
-ruby "1.9.3"
-gem 'rake'
+ruby "1.8.7"
+gem 'rake', '~> 10.0.3'
gem 'sass', '3.1.15'
gem 'bourbon', '~> 1.3.6'
-gem 'colorize'
-gem 'launchy'
+gem 'colorize', '~> 0.5.8'
+gem 'launchy', '~> 2.1.2'
From ad2dc8a322d2304bcdbc30072a49e27548416f55 Mon Sep 17 00:00:00 2001
From: Calen Pennington
Date: Sat, 26 Jan 2013 00:10:20 -0500
Subject: [PATCH 73/82] Add a .ruby-version file for use w/ rbenv
---
.ruby-version | 1 +
1 file changed, 1 insertion(+)
create mode 100644 .ruby-version
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 0000000000..e46f918393
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+1.8.7-p371
From 439acf2df7581b64021f6ed1fc1a2fa1d0676a2b Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 26 Jan 2013 13:06:09 -0500
Subject: [PATCH 74/82] Fix docstring and test for get_cohort
Returns None if course isn't cohorted, even if cohorts exist. Shouldn't happen on prod, but could if cohorting is turned off mid-course.
---
common/djangoapps/course_groups/cohorts.py | 10 +++--
.../djangoapps/course_groups/tests/tests.py | 43 ++++++++-----------
2 files changed, 24 insertions(+), 29 deletions(-)
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index 3fe333f9c8..df8b22ef26 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -70,15 +70,19 @@ def get_cohort(user, course_id):
def _get_cohort(user, course_id):
"""
- Given a django User and a course_id, return the user's cohort. In classes with
- auto-cohorting, put the user in a cohort if they aren't in one already.
+ Given a django User and a course_id, return the user's cohort in that
+ cohort.
+
+ TODO: In classes with auto-cohorting, put the user in a cohort if they
+ aren't in one already.
Arguments:
user: a Django User object.
course_id: string in the format 'org/course/run'
Returns:
- A CourseUserGroup object if the User has a cohort, or None.
+ A CourseUserGroup object if the course is cohorted and the User has a
+ cohort, else None.
Raises:
ValueError if the course_id doesn't exist.
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
index 509797b6f6..0303eaaa55 100644
--- a/common/djangoapps/course_groups/tests/tests.py
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -1,32 +1,14 @@
-from nose import SkipTest
import django.test
from django.contrib.auth.models import User
from django.conf import settings
from override_settings import override_settings
-
from course_groups.models import CourseUserGroup
from course_groups.cohorts import get_cohort, get_course_cohorts
-from xmodule.tests.test_import import BaseCourseTestCase
from xmodule.modulestore.django import modulestore
-def xml_store_config(data_dir):
- return {
- 'default': {
- 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
- 'OPTIONS': {
- 'data_dir': data_dir,
- 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
- }
- }
-}
-
-TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
-TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
-
-@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
class TestCohorts(django.test.TestCase):
def test_get_cohort(self):
@@ -36,22 +18,31 @@ class TestCohorts(django.test.TestCase):
# Proper fix: give all tests a standard modulestore that uses the test
# dir.
- raise SkipTest()
course = modulestore().get_course("edX/toy/2012_Fall")
- cohort = CourseUserGroup.objects.create(name="TestCohort",
- course_id=course.id,
- group_type=CourseUserGroup.COHORT)
+ self.assertEqual(course.id, "edX/toy/2012_Fall")
user = User.objects.create(username="test", email="a@b.com")
other_user = User.objects.create(username="test2", email="a2@b.com")
+ self.assertIsNone(get_cohort(user, course.id), "No cohort created yet")
+
+ cohort = CourseUserGroup.objects.create(name="TestCohort",
+ course_id=course.id,
+ group_type=CourseUserGroup.COHORT)
+
cohort.users.add(user)
- got = get_cohort(user, course.id)
- self.assertEquals(got.id, cohort.id, "Should find the right cohort")
+ self.assertIsNone(get_cohort(user, course.id),
+ "Course isn't cohorted, so shouldn't have a cohort")
- got = get_cohort(other_user, course.id)
- self.assertEquals(got, None, "other_user shouldn't have a cohort")
+ # Make the course cohorted...
+ course.metadata["cohort_config"] = {"cohorted": True}
+
+ self.assertEquals(get_cohort(user, course.id).id, cohort.id,
+ "Should find the right cohort")
+
+ self.assertEquals(get_cohort(other_user, course.id), None,
+ "other_user shouldn't have a cohort")
def test_get_course_cohorts(self):
From 46570b01efe7b0d0464798f88295e427208f6b0b Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sat, 26 Jan 2013 13:49:12 -0500
Subject: [PATCH 75/82] refactor tests, add test for is_commentable_cohorted,
fix a bug
---
common/djangoapps/course_groups/cohorts.py | 37 ++----
.../djangoapps/course_groups/tests/tests.py | 113 +++++++++++++++++-
2 files changed, 121 insertions(+), 29 deletions(-)
diff --git a/common/djangoapps/course_groups/cohorts.py b/common/djangoapps/course_groups/cohorts.py
index df8b22ef26..f84e18b214 100644
--- a/common/djangoapps/course_groups/cohorts.py
+++ b/common/djangoapps/course_groups/cohorts.py
@@ -35,8 +35,12 @@ def get_cohort_id(user, course_id):
def is_commentable_cohorted(course_id, commentable_id):
"""
- Given a course and a commentable id, return whether or not this commentable
- is cohorted.
+ Args:
+ course_id: string
+ commentable_id: string
+
+ Returns:
+ Bool: is this commentable cohorted?
Raises:
Http404 if the course doesn't exist.
@@ -49,7 +53,7 @@ def is_commentable_cohorted(course_id, commentable_id):
elif commentable_id in course.top_level_discussion_topic_ids:
# top level discussions have to be manually configured as cohorted
# (default is not)
- ans = commentable_id in course.cohorted_discussions()
+ ans = commentable_id in course.cohorted_discussions
else:
# inline discussions are cohorted by default
ans = True
@@ -61,21 +65,10 @@ def is_commentable_cohorted(course_id, commentable_id):
def get_cohort(user, course_id):
- c = _get_cohort(user, course_id)
- log.debug("get_cohort({0}, {1}) = {2}".format(
- user, course_id,
- c.id if c is not None else None))
- return c
-
-
-def _get_cohort(user, course_id):
"""
Given a django User and a course_id, return the user's cohort in that
cohort.
- TODO: In classes with auto-cohorting, put the user in a cohort if they
- aren't in one already.
-
Arguments:
user: a Django User object.
course_id: string in the format 'org/course/run'
@@ -88,7 +81,7 @@ def _get_cohort(user, course_id):
ValueError if the course_id doesn't exist.
"""
# First check whether the course is cohorted (users shouldn't be in a cohort
- # in non-cohorted courses, but settings can change after )
+ # in non-cohorted courses, but settings can change after course starts)
try:
course = courses.get_course_by_id(course_id)
except Http404:
@@ -98,17 +91,12 @@ def _get_cohort(user, course_id):
return None
try:
- group = CourseUserGroup.objects.get(course_id=course_id,
+ return CourseUserGroup.objects.get(course_id=course_id,
group_type=CourseUserGroup.COHORT,
users__id=user.id)
except CourseUserGroup.DoesNotExist:
- group = None
-
- if group:
- return group
-
- # TODO: add auto-cohorting logic here once we know what that will be.
- return None
+ # TODO: add auto-cohorting logic here once we know what that will be.
+ return None
def get_course_cohorts(course_id):
@@ -119,7 +107,8 @@ def get_course_cohorts(course_id):
course_id: string in the format 'org/course/run'
Returns:
- A list of CourseUserGroup objects. Empty if there are no cohorts.
+ A list of CourseUserGroup objects. Empty if there are no cohorts. Does
+ not check whether the course is cohorted.
"""
return list(CourseUserGroup.objects.filter(course_id=course_id,
group_type=CourseUserGroup.COHORT))
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
index 0303eaaa55..21fad8bbeb 100644
--- a/common/djangoapps/course_groups/tests/tests.py
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -5,12 +5,68 @@ from django.conf import settings
from override_settings import override_settings
from course_groups.models import CourseUserGroup
-from course_groups.cohorts import get_cohort, get_course_cohorts
+from course_groups.cohorts import (get_cohort, get_course_cohorts,
+ is_commentable_cohorted)
-from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.django import modulestore, _MODULESTORES
class TestCohorts(django.test.TestCase):
+
+ @staticmethod
+ def topic_name_to_id(course, name):
+ """
+ Given a discussion topic name, return an id for that name (includes
+ course and url_name).
+ """
+ return "{course}_{run}_{name}".format(course=course.location.course,
+ run=course.url_name,
+ name=name)
+
+
+ @staticmethod
+ def config_course_cohorts(course, discussions,
+ cohorted, cohorted_discussions=None):
+ """
+ Given a course with no discussion set up, add the discussions and set
+ the cohort config appropriately.
+
+ Arguments:
+ course: CourseDescriptor
+ discussions: list of topic names strings. Picks ids and sort_keys
+ automatically.
+ cohorted: bool.
+ cohorted_discussions: optional list of topic names. If specified,
+ converts them to use the same ids as topic names.
+
+ Returns:
+ Nothing -- modifies course in place.
+ """
+ def to_id(name):
+ return TestCohorts.topic_name_to_id(course, name)
+
+ topics = dict((name, {"sort_key": "A",
+ "id": to_id(name)})
+ for name in discussions)
+
+ course.metadata["discussion_topics"] = topics
+
+ d = {"cohorted": cohorted}
+ if cohorted_discussions is not None:
+ d["cohorted_discussions"] = [to_id(name)
+ for name in cohorted_discussions]
+ course.metadata["cohort_config"] = d
+
+
+ def setUp(self):
+ """
+ Make sure that course is reloaded every time--clear out the modulestore.
+ """
+ # don't like this, but don't know a better way to undo all changes made
+ # to course. We don't have a course.clone() method.
+ _MODULESTORES.clear()
+
+
def test_get_cohort(self):
# Need to fix this, but after we're testing on staging. (Looks like
# problem is that when get_cohort internally tries to look up the
@@ -20,7 +76,8 @@ class TestCohorts(django.test.TestCase):
# dir.
course = modulestore().get_course("edX/toy/2012_Fall")
self.assertEqual(course.id, "edX/toy/2012_Fall")
-
+ self.assertFalse(course.is_cohorted)
+
user = User.objects.create(username="test", email="a@b.com")
other_user = User.objects.create(username="test2", email="a2@b.com")
@@ -36,8 +93,8 @@ class TestCohorts(django.test.TestCase):
"Course isn't cohorted, so shouldn't have a cohort")
# Make the course cohorted...
- course.metadata["cohort_config"] = {"cohorted": True}
-
+ self.config_course_cohorts(course, [], cohorted=True)
+
self.assertEquals(get_cohort(user, course.id).id, cohort.id,
"Should find the right cohort")
@@ -65,3 +122,49 @@ class TestCohorts(django.test.TestCase):
cohorts = sorted([c.name for c in get_course_cohorts(course1_id)])
self.assertEqual(cohorts, ['TestCohort', 'TestCohort2'])
+
+ def test_is_commentable_cohorted(self):
+ course = modulestore().get_course("edX/toy/2012_Fall")
+ self.assertFalse(course.is_cohorted)
+
+ def to_id(name):
+ return self.topic_name_to_id(course, name)
+
+ # no topics
+ self.assertFalse(is_commentable_cohorted(course.id, to_id("General")),
+ "Course doesn't even have a 'General' topic")
+
+ # not cohorted
+ self.config_course_cohorts(course, ["General", "Feedback"],
+ cohorted=False)
+
+ self.assertFalse(is_commentable_cohorted(course.id, to_id("General")),
+ "Course isn't cohorted")
+
+ # cohorted, but top level topics aren't
+ self.config_course_cohorts(course, ["General", "Feedback"],
+ cohorted=True)
+
+ self.assertTrue(course.is_cohorted)
+ self.assertFalse(is_commentable_cohorted(course.id, to_id("General")),
+ "Course is cohorted, but 'General' isn't.")
+
+ self.assertTrue(
+ is_commentable_cohorted(course.id, to_id("random")),
+ "Non-top-level discussion is always cohorted in cohorted courses.")
+
+ # cohorted, including "Feedback" top-level topics aren't
+ self.config_course_cohorts(course, ["General", "Feedback"],
+ cohorted=True,
+ cohorted_discussions=["Feedback"])
+
+ self.assertTrue(course.is_cohorted)
+ self.assertFalse(is_commentable_cohorted(course.id, to_id("General")),
+ "Course is cohorted, but 'General' isn't.")
+
+ self.assertTrue(
+ is_commentable_cohorted(course.id, to_id("Feedback")),
+ "Feedback was listed as cohorted. Should be.")
+
+
+
From eff5093cbf5285c3d9dafd36ce0ae9a3faaf1161 Mon Sep 17 00:00:00 2001
From: David Ormsbee
Date: Sat, 26 Jan 2013 14:38:49 -0500
Subject: [PATCH 76/82] Force charset for application.sass to be UTF-8
---
lms/static/sass/application.scss | 1 +
1 file changed, 1 insertion(+)
diff --git a/lms/static/sass/application.scss b/lms/static/sass/application.scss
index 519118af84..f8d67987a0 100644
--- a/lms/static/sass/application.scss
+++ b/lms/static/sass/application.scss
@@ -1,3 +1,4 @@
+@charset "UTF-8";
@import 'bourbon/bourbon';
@import 'base/reset';
From 956ccb06e97ea0b6c0c06313191598909f5ce22f Mon Sep 17 00:00:00 2001
From: e0d
Date: Sat, 26 Jan 2013 21:10:14 -0500
Subject: [PATCH 77/82] explicitly set the domain for the csrf cookie to match
the session domain.
---
lms/envs/aws.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index a4aeb34a20..47bffac91e 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -37,6 +37,7 @@ with open(ENV_ROOT / "env.json") as env_file:
SITE_NAME = ENV_TOKENS['SITE_NAME']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
+CSRF_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
BOOK_URL = ENV_TOKENS['BOOK_URL']
MEDIA_URL = ENV_TOKENS['MEDIA_URL']
From d747f54fc89f96f68ad69d07c2c9f63b4bd4a930 Mon Sep 17 00:00:00 2001
From: e0d
Date: Sat, 26 Jan 2013 21:29:05 -0500
Subject: [PATCH 78/82] adding explicit charset declaration to additional css
---
lms/static/sass/course.scss | 1 +
lms/static/sass/ie.scss | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/lms/static/sass/course.scss b/lms/static/sass/course.scss
index e900e589b2..c0dfd9ef1f 100644
--- a/lms/static/sass/course.scss
+++ b/lms/static/sass/course.scss
@@ -1,3 +1,4 @@
+@charset "UTF-8";
@import 'bourbon/bourbon';
@import 'base/reset';
diff --git a/lms/static/sass/ie.scss b/lms/static/sass/ie.scss
index 11072570ec..d607e71ce0 100644
--- a/lms/static/sass/ie.scss
+++ b/lms/static/sass/ie.scss
@@ -1,3 +1,4 @@
+@charset "UTF-8";
@import "bourbon/bourbon";
@import "base/variables";
@@ -182,4 +183,4 @@ div.course-wrapper {
float: left !important;
width: 50px;
}
-}
\ No newline at end of file
+}
From c1f7b109d894be50e156718c890077e72934ce53 Mon Sep 17 00:00:00 2001
From: Victor Shnayder
Date: Sun, 27 Jan 2013 10:19:36 -0500
Subject: [PATCH 79/82] Fix missing import
---
lms/djangoapps/instructor/views.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index f0a0ef324a..a707506045 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -6,6 +6,7 @@ import itertools
import json
import logging
import os
+import re
import requests
import urllib
import json
From 313eab90c3ba817073289170b110bf00c3c7724c Mon Sep 17 00:00:00 2001
From: e0d
Date: Sun, 27 Jan 2013 17:18:55 -0500
Subject: [PATCH 80/82] add
---
lms/static/sass/application.scss | 1 -
1 file changed, 1 deletion(-)
diff --git a/lms/static/sass/application.scss b/lms/static/sass/application.scss
index f8d67987a0..519118af84 100644
--- a/lms/static/sass/application.scss
+++ b/lms/static/sass/application.scss
@@ -1,4 +1,3 @@
-@charset "UTF-8";
@import 'bourbon/bourbon';
@import 'base/reset';
From 6a7997f9ba25a441a94653554dbb0aa04eb74457 Mon Sep 17 00:00:00 2001
From: e0d
Date: Sun, 27 Jan 2013 17:19:00 -0500
Subject: [PATCH 81/82] add
---
lms/static/sass/course.scss | 1 -
1 file changed, 1 deletion(-)
diff --git a/lms/static/sass/course.scss b/lms/static/sass/course.scss
index c0dfd9ef1f..e900e589b2 100644
--- a/lms/static/sass/course.scss
+++ b/lms/static/sass/course.scss
@@ -1,4 +1,3 @@
-@charset "UTF-8";
@import 'bourbon/bourbon';
@import 'base/reset';
From c8e6bedc1a471d220c993bcfd8246ea042cd346b Mon Sep 17 00:00:00 2001
From: e0d
Date: Sun, 27 Jan 2013 17:19:12 -0500
Subject: [PATCH 82/82] add
---
lms/static/sass/ie.scss | 1 -
1 file changed, 1 deletion(-)
diff --git a/lms/static/sass/ie.scss b/lms/static/sass/ie.scss
index d607e71ce0..4b0f5aa3c0 100644
--- a/lms/static/sass/ie.scss
+++ b/lms/static/sass/ie.scss
@@ -1,4 +1,3 @@
-@charset "UTF-8";
@import "bourbon/bourbon";
@import "base/variables";