sysadmin.py 27 KB
Newer Older
Carson Gee committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
"""
This module creates a sysadmin dashboard for managing and viewing
courses.
"""
import csv
import json
import logging
import os
import subprocess
import time
import StringIO

from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError
from django.http import HttpResponse, Http404
from django.utils.decorators import method_decorator
from django.utils.html import escape
22
from django.utils import timezone
Carson Gee committed
23 24 25 26 27 28 29 30 31
from django.utils.translation import ugettext as _
from django.views.decorators.cache import cache_control
from django.views.generic.base import TemplateView
from django.views.decorators.http import condition
from django_future.csrf import ensure_csrf_cookie
from edxmako.shortcuts import render_to_response
import mongoengine

from courseware.courses import get_course_by_id
32 33
import dashboard.git_import as git_import
from dashboard.git_import import GitImportError
34
from student.roles import CourseStaffRole, CourseInstructorRole
Carson Gee committed
35 36 37 38 39 40
from dashboard.models import CourseImportLog
from external_auth.models import ExternalAuthMap
from external_auth.views import generate_password
from student.models import CourseEnrollment, UserProfile, Registration
import track.views
from xmodule.contentstore.django import contentstore
41
from xmodule.modulestore import XML_MODULESTORE_TYPE
Carson Gee committed
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.xml import XMLModuleStore


log = logging.getLogger(__name__)


class SysadminDashboardView(TemplateView):
    """Base class for sysadmin dashboard views with common methods"""

    template_name = 'sysadmin_dashboard.html'

    def __init__(self, **kwargs):
        """
        Initialize base sysadmin dashboard class with modulestore,
        modulestore_type and return msg
        """

        self.def_ms = modulestore()
        self.is_using_mongo = True
        if isinstance(self.def_ms, XMLModuleStore):
            self.is_using_mongo = False
        self.msg = u''
        self.datatable = []
        super(SysadminDashboardView, self).__init__(**kwargs)

    @method_decorator(ensure_csrf_cookie)
    @method_decorator(login_required)
    @method_decorator(cache_control(no_cache=True, no_store=True,
                                    must_revalidate=True))
    @method_decorator(condition(etag_func=None))
    def dispatch(self, *args, **kwargs):
        return super(SysadminDashboardView, self).dispatch(*args, **kwargs)

    def get_courses(self):
        """ Get an iterable list of courses."""

        courses = self.def_ms.get_courses()
        courses = dict([c.id, c] for c in courses)  # no course directory

        return courses

    def return_csv(self, filename, header, data):
        """
        Convenient function for handling the http response of a csv.
        data should be iterable and is used to stream object over http
        """

        csv_file = StringIO.StringIO()
        writer = csv.writer(csv_file, dialect='excel', quotechar='"',
                            quoting=csv.QUOTE_ALL)

        writer.writerow(header)

        # Setup streaming of the data
        def read_and_flush():
            """Read and clear buffer for optimization"""
            csv_file.seek(0)
            csv_data = csv_file.read()
            csv_file.seek(0)
            csv_file.truncate()
            return csv_data

        def csv_data():
            """Generator for handling potentially large CSVs"""
            for row in data:
                writer.writerow(row)
            csv_data = read_and_flush()
            yield csv_data
        response = HttpResponse(csv_data(), mimetype='text/csv')
        response['Content-Disposition'] = 'attachment; filename={0}'.format(
            filename)
        return response


class Users(SysadminDashboardView):
    """
    The status view provides Web based user management, a listing of
    courses loaded, and user statistics
    """

    def fix_external_auth_map_passwords(self):
        """
        This corrects any passwords that have drifted from eamap to
        internal django auth.  Needs to be removed when fixed in external_auth
        """

        msg = ''
        for eamap in ExternalAuthMap.objects.all():
            euser = eamap.user
            epass = eamap.internal_password
            if euser is None:
                continue
            try:
                testuser = authenticate(username=euser.username, password=epass)
            except (TypeError, PermissionDenied, AttributeError), err:
                msg += _('Failed in authenticating {0}, error {1}\n'
                         ).format(euser, err)
                continue
            if testuser is None:
                msg += _('Failed in authenticating {0}\n').format(euser)
                msg += _('fixed password')
                euser.set_password(epass)
                euser.save()
                continue
        if not msg:
            msg = _('All ok!')
        return msg

    def create_user(self, uname, name, password=None):
        """ Creates a user (both SSL and regular)"""

        if not uname:
            return _('Must provide username')
        if not name:
            return _('Must provide full name')

        email_domain = getattr(settings, 'SSL_AUTH_EMAIL_DOMAIN', 'MIT.EDU')

        msg = u''
163
        if settings.FEATURES['AUTH_USE_CERTIFICATES']:
Carson Gee committed
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
            if not '@' in uname:
                email = '{0}@{1}'.format(uname, email_domain)
            else:
                email = uname
            if not email.endswith('@{0}'.format(email_domain)):
                msg += u'{0} @{1}'.format(_('email must end in'), email_domain)
                return msg
            mit_domain = 'ssl:MIT'
            if ExternalAuthMap.objects.filter(external_id=email,
                                              external_domain=mit_domain):
                msg += _('Failed - email {0} already exists as '
                         'external_id').format(email)
                return msg
            new_password = generate_password()
        else:
            if not password:
                return _('Password must be supplied if not using certificates')

            email = uname

            if not '@' in email:
                msg += _('email address required (not username)')
                return msg
            new_password = password

        user = User(username=uname, email=email, is_active=True)
        user.set_password(new_password)
        try:
            user.save()
        except IntegrityError:
            msg += _('Oops, failed to create user {0}, '
                     'IntegrityError').format(user)
            return msg

        reg = Registration()
        reg.register(user)

        profile = UserProfile(user=user)
        profile.name = name
        profile.save()

205
        if settings.FEATURES['AUTH_USE_CERTIFICATES']:
Carson Gee committed
206 207 208 209 210 211 212 213 214 215 216 217
            credential_string = getattr(settings, 'SSL_AUTH_DN_FORMAT_STRING',
                                        '/C=US/ST=Massachusetts/O=Massachusetts Institute of Technology/OU=Client CA v1/CN={0}/emailAddress={1}')
            credentials = credential_string.format(name, email)
            eamap = ExternalAuthMap(
                external_id=email,
                external_email=email,
                external_domain=mit_domain,
                external_name=name,
                internal_password=new_password,
                external_credentials=json.dumps(credentials),
            )
            eamap.user = user
218
            eamap.dtsignup = timezone.now()
Carson Gee committed
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
            eamap.save()

        msg += _('User {0} created successfully!').format(user)
        return msg

    def delete_user(self, uname):
        """Deletes a user from django auth"""

        if not uname:
            return _('Must provide username')
        if '@' in uname:
            try:
                user = User.objects.get(email=uname)
            except User.DoesNotExist, err:
                msg = _('Cannot find user with email address {0}').format(uname)
                return msg
        else:
            try:
                user = User.objects.get(username=uname)
            except User.DoesNotExist, err:
                msg = _('Cannot find user with username {0} - {1}'
                        ).format(uname, str(err))
                return msg
        user.delete()
        return _('Deleted user {0}').format(uname)

    def make_common_context(self):
        """Returns the datatable used for this view"""

        self.datatable = {}
        courses = self.get_courses()

        self.datatable = dict(header=[_('Statistic'), _('Value')],
                              title=_('Site statistics'))
        self.datatable['data'] = [[_('Total number of users'),
                                   User.objects.all().count()]]

        self.msg += u'<h2>{0}</h2>'.format(
            _('Courses loaded in the modulestore'))
        self.msg += u'<ol>'
        for (cdir, course) in courses.items():
            self.msg += u'<li>{0} ({1})</li>'.format(
                escape(cdir), course.location.url())
        self.msg += u'</ol>'

    def get(self, request):

        if not request.user.is_staff:
            raise Http404
        self.make_common_context()

        context = {
            'datatable': self.datatable,
            'msg': self.msg,
            'djangopid': os.getpid(),
            'modeflag': {'users': 'active-section'},
275
            'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''),
Carson Gee committed
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
        }
        return render_to_response(self.template_name, context)

    def post(self, request):
        """Handle various actions available on page"""

        if not request.user.is_staff:
            raise Http404

        self.make_common_context()

        action = request.POST.get('action', '')
        track.views.server_track(request, action, {}, page='user_sysdashboard')

        if action == 'download_users':
            header = [_('username'), _('email'), ]
            data = ([u.username, u.email] for u in
                    (User.objects.all().iterator()))
            return self.return_csv('users_{0}.csv'.format(
                request.META['SERVER_NAME']), header, data)
        elif action == 'repair_eamap':
            self.msg = u'<h4>{0}</h4><pre>{1}</pre>{2}'.format(
                _('Repair Results'),
                self.fix_external_auth_map_passwords(),
                self.msg)
            self.datatable = {}
        elif action == 'create_user':
            uname = request.POST.get('student_uname', '').strip()
            name = request.POST.get('student_fullname', '').strip()
            password = request.POST.get('student_password', '').strip()
            self.msg = u'<h4>{0}</h4><p>{1}</p><hr />{2}'.format(
                _('Create User Results'),
                self.create_user(uname, name, password), self.msg)
        elif action == 'del_user':
            uname = request.POST.get('student_uname', '').strip()
            self.msg = u'<h4>{0}</h4><p>{1}</p><hr />{2}'.format(
                _('Delete User Results'), self.delete_user(uname), self.msg)

        context = {
            'datatable': self.datatable,
            'msg': self.msg,
            'djangopid': os.getpid(),
            'modeflag': {'users': 'active-section'},
319
            'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''),
Carson Gee committed
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
        }
        return render_to_response(self.template_name, context)


class Courses(SysadminDashboardView):
    """
    This manages adding/updating courses from git, deleting courses, and
    provides course listing information.
    """

    def git_info_for_course(self, cdir):
        """This pulls out some git info like the last commit"""

        cmd = ''
        gdir = settings.DATA_DIR / cdir
        info = ['', '', '']
        if not os.path.exists(gdir):
            return info

        cmd = ['git', 'log', '-1',
               '--format=format:{ "commit": "%H", "author": "%an %ae", "date": "%ad"}', ]
        try:
            output_json = json.loads(subprocess.check_output(cmd, cwd=gdir))
            info = [output_json['commit'],
                    output_json['date'],
                    output_json['author'], ]
        except (ValueError, subprocess.CalledProcessError):
            pass

        return info

351
    def get_course_from_git(self, gitloc, branch, datatable):
Carson Gee committed
352 353 354 355 356 357 358 359
        """This downloads and runs the checks for importing a course in git"""

        if not (gitloc.endswith('.git') or gitloc.startswith('http:') or
                gitloc.startswith('https:') or gitloc.startswith('git:')):
            return _("The git repo location should end with '.git', "
                     "and be a valid url")

        if self.is_using_mongo:
360
            return self.import_mongo_course(gitloc, branch)
Carson Gee committed
361

362
        return self.import_xml_course(gitloc, branch, datatable)
Carson Gee committed
363

364
    def import_mongo_course(self, gitloc, branch):
Carson Gee committed
365 366 367 368 369 370 371
        """
        Imports course using management command and captures logging output
        at debug level for display in template
        """

        msg = u''

372
        log.debug('Adding course using git repo {0}'.format(gitloc))
Carson Gee committed
373 374 375 376 377 378 379

        # Grab logging output for debugging imports
        output = StringIO.StringIO()
        import_log_handler = logging.StreamHandler(output)
        import_log_handler.setLevel(logging.DEBUG)

        logger_names = ['xmodule.modulestore.xml_importer',
380 381 382
                        'dashboard.git_import',
                        'xmodule.modulestore.xml',
                        'xmodule.seq_module', ]
Carson Gee committed
383 384 385 386 387 388 389 390
        loggers = []

        for logger_name in logger_names:
            logger = logging.getLogger(logger_name)
            logger.setLevel(logging.DEBUG)
            logger.addHandler(import_log_handler)
            loggers.append(logger)

391 392
        error_msg = ''
        try:
393
            git_import.add_repo(gitloc, None, branch)
394 395
        except GitImportError as ex:
            error_msg = str(ex)
Carson Gee committed
396 397 398 399
        ret = output.getvalue()

        # Remove handler hijacks
        for logger in loggers:
400
            logger.setLevel(logging.NOTSET)
Carson Gee committed
401 402
            logger.removeHandler(import_log_handler)

403 404 405 406 407 408 409 410 411
        if error_msg:
            msg_header = error_msg
            color = 'red'
        else:
            msg_header = _('Added Course')
            color = 'blue'

        msg = u"<h4 style='color:{0}'>{1}</h4>".format(color, msg_header)
        msg += "<pre>{0}</pre>".format(escape(ret))
Carson Gee committed
412 413
        return msg

414
    def import_xml_course(self, gitloc, branch, datatable):
Carson Gee committed
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
        """Imports a git course into the XMLModuleStore"""

        msg = u''
        if not getattr(settings, 'GIT_IMPORT_WITH_XMLMODULESTORE', False):
            return _('Refusing to import. GIT_IMPORT_WITH_XMLMODULESTORE is '
                     'not turned on, and it is generally not safe to import '
                     'into an XMLModuleStore with multithreaded. We '
                     'recommend you enable the MongoDB based module store '
                     'instead, unless this is a development environment.')
        cdir = (gitloc.rsplit('/', 1)[1])[:-4]
        gdir = settings.DATA_DIR / cdir
        if os.path.exists(gdir):
            msg += _("The course {0} already exists in the data directory! "
                     "(reloading anyway)").format(cdir)
            cmd = ['git', 'pull', ]
            cwd = gdir
        else:
            cmd = ['git', 'clone', gitloc, ]
            cwd = settings.DATA_DIR
        cwd = os.path.abspath(cwd)
        try:
436 437 438
            cmd_output = escape(
                subprocess.check_output(cmd, stderr=subprocess.STDOUT, cwd=cwd)
            )
439 440
        except subprocess.CalledProcessError as ex:
            log.exception('Git pull or clone output was: %r', ex.output)
441 442 443 444
            # Translators: unable to download the course content from
            # the source git repository. Clone occurs if this is brand
            # new, and pull is when it is being updated from the
            # source.
445 446
            return _('Unable to clone or pull repository. Please check '
                     'your url. Output was: {0!r}'.format(ex.output))
Carson Gee committed
447 448 449 450 451

        msg += u'<pre>{0}</pre>'.format(cmd_output)
        if not os.path.exists(gdir):
            msg += _('Failed to clone repository to {0}').format(gdir)
            return msg
452 453 454 455 456 457
        # Change branch if specified
        if branch:
            try:
                git_import.switch_branch(branch, gdir)
            except GitImportError as ex:
                return str(ex)
458 459 460 461 462
            # Translators: This is a git repository branch, which is a
            # specific version of a courses content
            msg += u'<p>{0}</p>'.format(
                _('Successfully switched to branch: '
                  '{branch_name}'.format(branch_name=branch)))
463

Carson Gee committed
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
        self.def_ms.try_load_course(os.path.abspath(gdir))
        errlog = self.def_ms.errored_courses.get(cdir, '')
        if errlog:
            msg += u'<hr width="50%"><pre>{0}</pre>'.format(escape(errlog))
        else:
            course = self.def_ms.courses[os.path.abspath(gdir)]
            msg += _('Loaded course {0} {1}<br/>Errors:').format(
                cdir, course.display_name)
            errors = self.def_ms.get_item_errors(course.location)
            if not errors:
                msg += u'None'
            else:
                msg += u'<ul>'
                for (summary, err) in errors:
                    msg += u'<li><pre>{0}: {1}</pre></li>'.format(escape(summary),
                                                                  escape(err))
                msg += u'</ul>'
            datatable['data'].append([course.display_name, cdir]
                                     + self.git_info_for_course(cdir))
        return msg

    def make_datatable(self):
        """Creates course information datatable"""

        data = []
        courses = self.get_courses()

        for (cdir, course) in courses.items():
            gdir = cdir
            if '/' in cdir:
                gdir = cdir.rsplit('/', 1)[1]
            data.append([course.display_name, cdir]
                        + self.git_info_for_course(gdir))

        return dict(header=[_('Course Name'), _('Directory/ID'),
                            _('Git Commit'), _('Last Change'),
                            _('Last Editor')],
                    title=_('Information about all courses'),
                    data=data)

    def get(self, request):
        """Displays forms and course information"""

        if not request.user.is_staff:
            raise Http404

        context = {
            'datatable': self.make_datatable(),
            'msg': self.msg,
            'djangopid': os.getpid(),
            'modeflag': {'courses': 'active-section'},
515
            'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''),
Carson Gee committed
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
        }
        return render_to_response(self.template_name, context)

    def post(self, request):
        """Handle all actions from courses view"""

        if not request.user.is_staff:
            raise Http404

        action = request.POST.get('action', '')
        track.views.server_track(request, action, {},
                                 page='courses_sysdashboard')

        courses = self.get_courses()
        if action == 'add_course':
            gitloc = request.POST.get('repo_location', '').strip().replace(' ', '').replace(';', '')
532
            branch = request.POST.get('repo_branch', '').strip().replace(' ', '').replace(';', '')
Carson Gee committed
533
            datatable = self.make_datatable()
534
            self.msg += self.get_course_from_git(gitloc, branch, datatable)
Carson Gee committed
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551

        elif action == 'del_course':
            course_id = request.POST.get('course_id', '').strip()
            course_found = False
            if course_id in courses:
                course_found = True
                course = courses[course_id]
            else:
                try:
                    course = get_course_by_id(course_id)
                    course_found = True
                except Exception, err:   # pylint: disable=broad-except
                    self.msg += _('Error - cannot get course with ID '
                                  '{0}<br/><pre>{1}</pre>').format(
                                      course_id, escape(str(err))
                                  )

552 553
            is_xml_course = (modulestore().get_modulestore_type(course_id) == XML_MODULESTORE_TYPE)
            if course_found and is_xml_course:
Carson Gee committed
554 555 556 557 558 559 560 561 562 563 564 565 566 567
                cdir = course.data_dir
                self.def_ms.courses.pop(cdir)

                # now move the directory (don't actually delete it)
                new_dir = "{course_dir}_deleted_{timestamp}".format(
                    course_dir=cdir,
                    timestamp=int(time.time())
                )
                os.rename(settings.DATA_DIR / cdir, settings.DATA_DIR / new_dir)

                self.msg += (u"<font color='red'>Deleted "
                             u"{0} = {1} ({2})</font>".format(
                                 cdir, course.id, course.display_name))

568
            elif course_found and not is_xml_course:
Carson Gee committed
569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
                # delete course that is stored with mongodb backend
                loc = course.location
                content_store = contentstore()
                commit = True
                delete_course(self.def_ms, content_store, loc, commit)
                # don't delete user permission groups, though
                self.msg += \
                    u"<font color='red'>{0} {1} = {2} ({3})</font>".format(
                        _('Deleted'), loc, course.id, course.display_name)
            datatable = self.make_datatable()

        context = {
            'datatable': datatable,
            'msg': self.msg,
            'djangopid': os.getpid(),
            'modeflag': {'courses': 'active-section'},
585
            'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''),
Carson Gee committed
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623
        }
        return render_to_response(self.template_name, context)


class Staffing(SysadminDashboardView):
    """
    The status view provides a view of staffing and enrollment in
    courses that include an option to download the data as a csv.
    """

    def get(self, request):
        """Displays course Enrollment and staffing course statistics"""

        if not request.user.is_staff:
            raise Http404
        data = []

        courses = self.get_courses()

        for (cdir, course) in courses.items():  # pylint: disable=unused-variable
            datum = [course.display_name, course.id]
            datum += [CourseEnrollment.objects.filter(
                course_id=course.id).count()]
            datum += [CourseStaffRole(course.location).users_with_role().count()]
            datum += [','.join([x.username for x in CourseInstructorRole(
                course.location).users_with_role()])]
            data.append(datum)

        datatable = dict(header=[_('Course Name'), _('course_id'),
                                 _('# enrolled'), _('# staff'),
                                 _('instructors')],
                         title=_('Enrollment information for all courses'),
                         data=data)
        context = {
            'datatable': datatable,
            'msg': self.msg,
            'djangopid': os.getpid(),
            'modeflag': {'staffing': 'active-section'},
624
            'edx_platform_version': getattr(settings, 'EDX_PLATFORM_VERSION_STRING', ''),
Carson Gee committed
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694
        }
        return render_to_response(self.template_name, context)

    def post(self, request):
        """Handle all actions from staffing and enrollment view"""

        action = request.POST.get('action', '')
        track.views.server_track(request, action, {},
                                 page='staffing_sysdashboard')

        if action == 'get_staff_csv':
            data = []
            roles = [CourseInstructorRole, CourseStaffRole, ]

            courses = self.get_courses()

            for (cdir, course) in courses.items():  # pylint: disable=unused-variable
                for role in roles:
                    for user in role(course.location).users_with_role():
                        datum = [course.id, role, user.username, user.email,
                                 user.profile.name]
                        data.append(datum)
            header = [_('course_id'),
                      _('role'), _('username'),
                      _('email'), _('full_name'), ]
            return self.return_csv('staff_{0}.csv'.format(
                request.META['SERVER_NAME']), header, data)

        return self.get(request)


class GitLogs(TemplateView):
    """
    This provides a view into the import of courses from git repositories.
    It is convenient for allowing course teams to see what may be wrong with
    their xml
    """

    template_name = 'sysadmin_dashboard_gitlogs.html'

    @method_decorator(login_required)
    def get(self, request, *args, **kwargs):
        """Shows logs of imports that happened as a result of a git import"""

        course_id = kwargs.get('course_id')

        # Set mongodb defaults even if it isn't defined in settings
        mongo_db = {
            'host': 'localhost',
            'user': '',
            'password': '',
            'db': 'xlog',
        }

        # Allow overrides
        if hasattr(settings, 'MONGODB_LOG'):
            for config_item in ['host', 'user', 'password', 'db', ]:
                mongo_db[config_item] = settings.MONGODB_LOG.get(
                    config_item, mongo_db[config_item])

        mongouri = 'mongodb://{user}:{password}@{host}/{db}'.format(**mongo_db)

        error_msg = ''

        try:
            if mongo_db['user'] and mongo_db['password']:
                mdb = mongoengine.connect(mongo_db['db'], host=mongouri)
            else:
                mdb = mongoengine.connect(mongo_db['db'], host=mongo_db['host'])
        except mongoengine.connection.ConnectionError:
695
            log.exception('Unable to connect to mongodb to save log, '
696
                          'please check MONGODB_LOG settings.')
Carson Gee committed
697 698 699 700 701 702 703 704 705 706

        if course_id is None:
            # Require staff if not going to specific course
            if not request.user.is_staff:
                raise Http404
            cilset = CourseImportLog.objects.all().order_by('-created')
        else:
            try:
                course = get_course_by_id(course_id)
            except Exception:  # pylint: disable=broad-except
707 708
                log.info('Cannot find course {0}'.format(course_id))
                raise Http404
Carson Gee committed
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724

            # Allow only course team, instructors, and staff
            if not (request.user.is_staff or
                    CourseInstructorRole(course.location).has_user(request.user) or
                    CourseStaffRole(course.location).has_user(request.user)):
                raise Http404
            log.debug('course_id={0}'.format(course_id))
            cilset = CourseImportLog.objects.filter(
                course_id=course_id).order_by('-created')
            log.debug('cilset length={0}'.format(len(cilset)))
        mdb.disconnect()
        context = {'cilset': cilset,
                   'course_id': course_id,
                   'error_msg': error_msg}

        return render_to_response(self.template_name, context)