def run_course_update_command( request, repo, content_repo, pctx, command, new_sha, may_update, prevent_discarding_revisions): if command.startswith("fetch"): if command != "fetch": command = command[6:] if not pctx.course.git_source: raise RuntimeError(_("no git source URL specified")) client, remote_path = \ get_dulwich_client_and_remote_path_from_course(pctx.course) remote_refs = client.fetch(remote_path, repo) transfer_remote_refs(repo, remote_refs) remote_head = remote_refs[b"HEAD"] if ( prevent_discarding_revisions and is_parent_commit(repo, repo[remote_head], repo[b"HEAD"], max_history_check_size=20)): raise RuntimeError(_("fetch would discard commits, refusing")) repo[b"HEAD"] = remote_head messages.add_message(request, messages.SUCCESS, _("Fetch successful.")) new_sha = remote_head if command == "fetch": return if command == "end_preview": messages.add_message(request, messages.INFO, _("Preview ended.")) pctx.participation.preview_git_commit_sha = None pctx.participation.save() return # {{{ validate from course.validation import validate_course_content, ValidationError try: warnings = validate_course_content( content_repo, pctx.course.course_file, pctx.course.events_file, new_sha, course=pctx.course) except ValidationError as e: messages.add_message(request, messages.ERROR, _("Course content did not validate successfully. (%s) " "Update not applied.") % str(e)) return else: if not warnings: messages.add_message(request, messages.SUCCESS, _("Course content validated successfully.")) else: messages.add_message(request, messages.WARNING, string_concat( _("Course content validated OK, with warnings: "), "<ul>%s</ul>") % ("".join( "<li><i>%(location)s</i>: %(warningtext)s</li>" % {'location': w.location, 'warningtext': w.text} for w in warnings))) # }}} if command == "preview": messages.add_message(request, messages.INFO, _("Preview activated.")) pctx.participation.preview_git_commit_sha = new_sha.decode() pctx.participation.save() elif command == "update" and may_update: pctx.course.active_git_commit_sha = new_sha.decode() pctx.course.save() messages.add_message(request, messages.SUCCESS, _("Update applied. ")) else: raise RuntimeError(_("invalid command"))
def fetch_course_updates_inner(pctx): import sys if pctx.role != participation_role.instructor: raise PermissionDenied("must be instructor to fetch revisisons") form = GitFetchForm(pctx.request.POST, pctx.request.FILES) if pctx.request.method == "POST": if form.is_valid(): was_successful = True log_lines = [] try: repo = pctx.repo if not pctx.course.git_source: raise RuntimeError("no git source URL specified") log_lines.append("Pre-fetch head is at '%s'" % repo.head()) client, remote_path = \ get_dulwich_client_and_remote_path_from_course(pctx.course) remote_refs = client.fetch(remote_path, repo) remote_head = remote_refs["HEAD"] if is_parent_commit(repo, repo[remote_head], repo["HEAD"], max_history_check_size=10): raise RuntimeError("fetch would discard commits, refusing") repo["HEAD"] = remote_head log_lines.append("Post-fetch head is at '%s'" % repo.head()) except Exception: was_successful = False from traceback import format_exception log = "\n".join(log_lines) + "".join( format_exception(*sys.exc_info())) else: log = "\n".join(log_lines) if was_successful: messages.add_message(pctx.request, messages.SUCCESS, "Fetch successful.") return redirect( "course.versioning.update_course", pctx.course.identifier) return render_course_page(pctx, 'course/course-bulk-result.html', { "process_description": "Fetch course updates via git", "log": log, "status": "Fetch failed. See above for error.", "was_successful": was_successful, }) else: form = GitFetchForm() else: form = GitFetchForm() return render_course_page(pctx, "course/generic-course-form.html", { "form": form, "form_description": "Fetch New Course Revisions", })
def set_up_new_course(request): if not request.user.is_staff: raise PermissionDenied(_("only staff may create courses")) if request.method == "POST": form = CourseCreationForm(request.POST) if form.is_valid(): new_course = form.save(commit=False) from course.content import get_course_repo_path repo_path = get_course_repo_path(new_course) try: import os os.makedirs(repo_path) repo = None try: with transaction.atomic(): from dulwich.repo import Repo repo = Repo.init(repo_path) client, remote_path = \ get_dulwich_client_and_remote_path_from_course( new_course) remote_refs = client.fetch(remote_path, repo) transfer_remote_refs(repo, remote_refs) new_sha = repo[b"HEAD"] = remote_refs[b"HEAD"] vrepo = repo if new_course.course_root_path: from course.content import SubdirRepoWrapper vrepo = SubdirRepoWrapper( vrepo, new_course.course_root_path) from course.validation import validate_course_content validate_course_content( vrepo, new_course.course_file, new_course.events_file, new_sha) del repo del vrepo new_course.active_git_commit_sha = new_sha.decode() new_course.save() # {{{ set up a participation for the course creator part = Participation() part.user = request.user part.course = new_course part.role = participation_role.instructor part.status = participation_status.active part.save() # }}} messages.add_message(request, messages.INFO, _("Course content validated, creation " "succeeded.")) except: # Don't coalesce this handler with the one below. We only want # to delete the directory if we created it. Trust me. # Work around read-only files on Windows. # https://docs.python.org/3.5/library/shutil.html#rmtree-example import os import stat import shutil # Make sure files opened for 'repo' above are actually closed. if repo is not None: # noqa repo.close() # noqa def remove_readonly(func, path, _): # noqa "Clear the readonly bit and reattempt the removal" os.chmod(path, stat.S_IWRITE) func(path) try: shutil.rmtree(repo_path, onerror=remove_readonly) except OSError: messages.add_message(request, messages.WARNING, ugettext("Failed to delete unused " "repository directory '%s'.") % repo_path) raise except Exception as e: from traceback import print_exc print_exc() messages.add_message(request, messages.ERROR, string_concat( _("Course creation failed"), ": %(err_type)s: %(err_str)s") % {"err_type": type(e).__name__, "err_str": str(e)}) else: return redirect( "relate-course_page", new_course.identifier) else: form = CourseCreationForm() return render(request, "generic-form.html", { "form_description": _("Set up new course"), "form": form })
def run_course_update_command(request, repo, content_repo, pctx, command, new_sha, may_update, prevent_discarding_revisions): if command.startswith("fetch"): if command != "fetch": command = command[6:] if not pctx.course.git_source: raise RuntimeError(_("no git source URL specified")) client, remote_path = \ get_dulwich_client_and_remote_path_from_course(pctx.course) remote_refs = client.fetch(remote_path, repo) transfer_remote_refs(repo, remote_refs) remote_head = remote_refs[b"HEAD"] if (prevent_discarding_revisions and is_parent_commit( repo, repo[remote_head], repo[b"HEAD"], max_history_check_size=20)): raise RuntimeError(_("fetch would discard commits, refusing")) repo[b"HEAD"] = remote_head messages.add_message(request, messages.SUCCESS, _("Fetch successful.")) new_sha = remote_head if command == "fetch": return if command == "end_preview": pctx.participation.preview_git_commit_sha = None pctx.participation.save() messages.add_message(request, messages.INFO, _("Preview ended.")) return # {{{ validate from course.validation import validate_course_content, ValidationError try: warnings = validate_course_content(content_repo, pctx.course.course_file, pctx.course.events_file, new_sha, course=pctx.course) except ValidationError as e: messages.add_message( request, messages.ERROR, _("Course content did not validate successfully. (%s) " "Update not applied.") % str(e)) return else: if not warnings: messages.add_message(request, messages.SUCCESS, _("Course content validated successfully.")) else: messages.add_message( request, messages.WARNING, string_concat( _("Course content validated OK, with warnings: "), "<ul>%s</ul>") % ("".join("<li><i>%(location)s</i>: %(warningtext)s</li>" % { 'location': w.location, 'warningtext': w.text } for w in warnings))) # }}} if command == "preview": messages.add_message(request, messages.INFO, _("Preview activated.")) pctx.participation.preview_git_commit_sha = new_sha.decode() pctx.participation.save() elif command == "update" and may_update: pctx.course.active_git_commit_sha = new_sha.decode() pctx.course.save() if pctx.participation.preview_git_commit_sha is not None: pctx.participation.preview_git_commit_sha = None pctx.participation.save() messages.add_message(request, messages.INFO, _("Preview ended.")) messages.add_message(request, messages.SUCCESS, _("Update applied. ")) else: raise RuntimeError(_("invalid command"))
def set_up_new_course(request): if not request.user.is_staff: raise PermissionDenied("only staff may create courses") if request.method == "POST": form = CourseCreationForm(request.POST) if form.is_valid(): new_course = form.save(commit=False) from course.content import get_course_repo_path repo_path = get_course_repo_path(new_course) try: import os os.makedirs(repo_path) try: with transaction.atomic(): from dulwich.repo import Repo repo = Repo.init(repo_path) client, remote_path = \ get_dulwich_client_and_remote_path_from_course( new_course) remote_refs = client.fetch(remote_path, repo) new_sha = repo["HEAD"] = remote_refs["HEAD"] from course.validation import validate_course_content validate_course_content( repo, new_course.course_file, new_course.events_file, new_sha) new_course.valid = True new_course.active_git_commit_sha = new_sha new_course.save() # {{{ set up a participation for the course creator part = Participation() part.user = request.user part.course = new_course part.role = participation_role.instructor part.status = participation_status.active part.save() # }}} messages.add_message(request, messages.INFO, "Course content validated, creation succeeded. " "You may want to view the events used " "in the course content and create them. " + '<a href="%s" class="btn btn-primary">' 'Check »</a>' % reverse("course.calendar.check_events", args=(new_course.identifier,))) except: # Don't coalesce this handler with the one below. We only want # to delete the directory if we created it. Trust me. import shutil shutil.rmtree(repo_path) raise except Exception as e: from traceback import print_exc print_exc() messages.add_message(request, messages.ERROR, "Course creation failed: %s: %s" % ( type(e).__name__, str(e))) else: return redirect( "course.views.course_page", new_course.identifier) else: form = CourseCreationForm() return render(request, "generic-form.html", { "form_description": "Set up new course", "form": form })
def set_up_new_course(request): # type: (http.HttpRequest) -> http.HttpResponse if request.method == "POST": form = CourseCreationForm(request.POST) if form.is_valid(): new_course = form.save(commit=False) from course.content import get_course_repo_path repo_path = get_course_repo_path(new_course) try: import os os.makedirs(repo_path) repo = None try: with transaction.atomic(): repo = Repo.init(repo_path) client, remote_path = \ get_dulwich_client_and_remote_path_from_course( new_course) remote_refs = client.fetch(remote_path, repo) if remote_refs is None: raise RuntimeError( _("No refs found in remote repository" " (i.e. no master branch, no HEAD). " "This looks very much like a blank repository. " "Please create course.yml in the remote " "repository before creating your course.")) transfer_remote_refs(repo, remote_refs) new_sha = repo[b"HEAD"] = remote_refs[b"HEAD"] vrepo = repo if new_course.course_root_path: from course.content import SubdirRepoWrapper vrepo = SubdirRepoWrapper( vrepo, new_course.course_root_path) from course.validation import validate_course_content validate_course_content( # type: ignore vrepo, new_course.course_file, new_course.events_file, new_sha) del vrepo new_course.active_git_commit_sha = new_sha.decode() new_course.save() # {{{ set up a participation for the course creator part = Participation() part.user = request.user part.course = new_course part.status = participation_status.active part.save() part.roles.set([ # created by signal handler for course creation ParticipationRole.objects.get( course=new_course, identifier="instructor") ]) # }}} messages.add_message( request, messages.INFO, _("Course content validated, creation " "succeeded.")) except: # Don't coalesce this handler with the one below. We only want # to delete the directory if we created it. Trust me. # Work around read-only files on Windows. # https://docs.python.org/3.5/library/shutil.html#rmtree-example import os import stat import shutil # Make sure files opened for 'repo' above are actually closed. if repo is not None: # noqa repo.close() # noqa def remove_readonly(func, path, _): # noqa "Clear the readonly bit and reattempt the removal" os.chmod(path, stat.S_IWRITE) func(path) try: shutil.rmtree(repo_path, onerror=remove_readonly) except OSError: messages.add_message( request, messages.WARNING, ugettext("Failed to delete unused " "repository directory '%s'.") % repo_path) raise except Exception as e: from traceback import print_exc print_exc() messages.add_message( request, messages.ERROR, string_concat(_("Course creation failed"), ": %(err_type)s: %(err_str)s") % { "err_type": type(e).__name__, "err_str": str(e) }) else: return redirect("relate-course_page", new_course.identifier) else: form = CourseCreationForm() return render(request, "generic-form.html", { "form_description": _("Set up new course"), "form": form })
def run_course_update_command(request, repo, content_repo, pctx, command, new_sha, may_update, prevent_discarding_revisions): if command not in ALLOWED_COURSE_REVISIOIN_COMMANDS: raise RuntimeError(_("invalid command")) if command.startswith("fetch"): if command != "fetch": command = command[6:] client, remote_path = \ get_dulwich_client_and_remote_path_from_course(pctx.course) fetch_pack_result = client.fetch(remote_path, repo) assert isinstance(fetch_pack_result, dulwich.client.FetchPackResult) transfer_remote_refs(repo, fetch_pack_result) remote_head = fetch_pack_result.refs[b"HEAD"] if prevent_discarding_revisions: # Guard against bad scenario: # Local is not ancestor of remote, i.e. the branches have diverged. if not is_ancestor_commit(repo, repo[b"HEAD"], repo[remote_head], max_history_check_size=20) and \ repo[b"HEAD"] != repo[remote_head]: raise RuntimeError( _("internal git repo has more commits. Fetch, " "merge and push.")) repo[b"HEAD"] = remote_head messages.add_message(request, messages.SUCCESS, _("Fetch successful.")) new_sha = remote_head if command == "fetch": return if command == "end_preview": pctx.participation.preview_git_commit_sha = None pctx.participation.save() messages.add_message(request, messages.INFO, _("Preview ended.")) return # {{{ validate from course.validation import validate_course_content, ValidationError try: warnings = validate_course_content(content_repo, pctx.course.course_file, pctx.course.events_file, new_sha, course=pctx.course) except ValidationError as e: messages.add_message( request, messages.ERROR, _("Course content did not validate successfully: '%s' " "Update not applied.") % str(e)) return else: if not warnings: messages.add_message(request, messages.SUCCESS, _("Course content validated successfully.")) else: messages.add_message( request, messages.WARNING, string_concat( _("Course content validated OK, with warnings: "), "<ul>%s</ul>") % ("".join("<li><i>%(location)s</i>: %(warningtext)s</li>" % { 'location': w.location, 'warningtext': w.text } for w in warnings))) # }}} if command == "preview": messages.add_message(request, messages.INFO, _("Preview activated.")) pctx.participation.preview_git_commit_sha = new_sha.decode() pctx.participation.save() elif command == "update" and may_update: # pragma: no branch pctx.course.active_git_commit_sha = new_sha.decode() pctx.course.save() if pctx.participation.preview_git_commit_sha is not None: pctx.participation.preview_git_commit_sha = None pctx.participation.save() messages.add_message(request, messages.INFO, _("Preview ended.")) messages.add_message(request, messages.SUCCESS, _("Update applied. "))
def run_course_update_command(request, pctx, command, new_sha): if command.startswith("fetch_"): command = command[6:] if not pctx.course.git_source: raise RuntimeError("no git source URL specified") repo = pctx.repo client, remote_path = \ get_dulwich_client_and_remote_path_from_course(pctx.course) remote_refs = client.fetch(remote_path, repo) remote_head = remote_refs["HEAD"] if is_parent_commit(repo, repo[remote_head], repo["HEAD"], max_history_check_size=10): raise RuntimeError("fetch would discard commits, refusing") repo["HEAD"] = remote_head messages.add_message(request, messages.SUCCESS, "Fetch successful.") new_sha = repo.head() if command == "end_preview": messages.add_message(request, messages.INFO, "Preview ended.") pctx.participation.preview_git_commit_sha = None pctx.participation.save() return # {{{ validate from course.validation import validate_course_content, ValidationError try: warnings = validate_course_content( repo, pctx.course.course_file, pctx.course.events_file, new_sha) except ValidationError as e: messages.add_message(request, messages.ERROR, "Course content did not validate successfully. (%s) " "Update not applied." % str(e)) return else: if not warnings: messages.add_message(request, messages.SUCCESS, "Course content validated successfully.") else: messages.add_message(request, messages.WARNING, "Course content validated OK, with warnings:" "<ul>%s</ul>" % ("".join( "<li><i>%s</i>: %s</li>" % (w.location, w.text) for w in warnings))) # }}} if command == "preview": messages.add_message(request, messages.INFO, "Preview activated.") pctx.participation.preview_git_commit_sha = new_sha pctx.participation.save() elif command == "update": pctx.course.active_git_commit_sha = new_sha pctx.course.valid = True pctx.course.save() messages.add_message(request, messages.SUCCESS, "Update applied. " "You may want to view the events used " "in the course content and check that they " "are recognized. " + '<p><a href="%s" class="btn btn-primary" ' 'style="margin-top:8px">' 'Check »</a></p>' % reverse("course.calendar.check_events", args=(pctx.course.identifier,))) else: raise RuntimeError("invalid command")
def set_up_new_course(request): if not request.user.is_staff: raise PermissionDenied(_("only staff may create courses")) if request.method == "POST": form = CourseCreationForm(request.POST) if form.is_valid(): new_course = form.save(commit=False) from course.content import get_course_repo_path repo_path = get_course_repo_path(new_course) try: import os os.makedirs(repo_path) try: with transaction.atomic(): from dulwich.repo import Repo repo = Repo.init(repo_path) client, remote_path = \ get_dulwich_client_and_remote_path_from_course( new_course) remote_refs = client.fetch(remote_path, repo) new_sha = repo["HEAD"] = remote_refs["HEAD"] vrepo = repo if new_course.course_root_path: from course.content import SubdirRepoWrapper vrepo = SubdirRepoWrapper( vrepo, new_course.course_root_path) from course.validation import validate_course_content validate_course_content(vrepo, new_course.course_file, new_course.events_file, new_sha) del repo del vrepo new_course.valid = True new_course.active_git_commit_sha = new_sha new_course.save() # {{{ set up a participation for the course creator part = Participation() part.user = request.user part.course = new_course part.role = participation_role.instructor part.status = participation_status.active part.save() # }}} messages.add_message( request, messages.INFO, _("Course content validated, creation " "succeeded.")) except: # Don't coalesce this handler with the one below. We only want # to delete the directory if we created it. Trust me. # Work around read-only files on Windows. # https://docs.python.org/3.5/library/shutil.html#rmtree-example import os import stat import shutil # Make sure files opened for 'repo' above are actually closed. import gc gc.collect() def remove_readonly(func, path, _): # noqa "Clear the readonly bit and reattempt the removal" os.chmod(path, stat.S_IWRITE) func(path) try: shutil.rmtree(repo_path, onerror=remove_readonly) except OSError: messages.add_message( request, messages.WARNING, ugettext("Failed to delete unused " "repository directory '%s'.") % repo_path) raise except Exception as e: from traceback import print_exc print_exc() messages.add_message( request, messages.ERROR, string_concat(_("Course creation failed"), ": %(err_type)s: %(err_str)s") % { "err_type": type(e).__name__, "err_str": str(e) }) else: return redirect("relate-course_page", new_course.identifier) else: form = CourseCreationForm() return render(request, "generic-form.html", { "form_description": _("Set up new course"), "form": form })