예제 #1
0
def _show_components_staff(request, course_slug, activity_slug):
    """
    Show all the components of this activity
    Responsible for updating position
    """
    course = get_object_or_404(CourseOffering, slug=course_slug)
    activity = get_object_or_404(course.activity_set, slug=activity_slug, deleted=False)

    #if POST, update the positions
    if request.method == 'POST':
        component_list = select_all_components(activity, include_deleted=True)
        counter = 0
        for component in component_list:
            counter = counter + 1
            t = request.POST.get('' + str(counter) + '_position');
            #in case t is not a number
            try:
                component.position = int(t)
                component.save()
            except:
                pass
        messages.add_message(request, messages.SUCCESS, 'Component positions updated.')
        return HttpResponseRedirect(reverse(show_components, args=[course_slug, activity_slug]))
    
    component_list = select_all_components(activity, include_deleted=True)
    return render(request, "submission/component_view_staff.html",
        {"course":course, "activity":activity, "component_list":component_list})
예제 #2
0
def edit_single(request, course_slug, activity_slug):
    course = get_object_or_404(CourseOffering, slug=course_slug)
    activity = get_object_or_404(course.activity_set,
                                 slug=activity_slug,
                                 deleted=False)
    component_list = select_all_components(activity)

    #get component
    edit_id = request.GET.get('id')
    component = get_component(activity=activity, id=edit_id)
    if component is None:
        return NotFoundResponse(request)

    form = component.Type.ComponentForm(instance=component)

    #if form submitted
    if request.method == 'POST':
        new_form = component.Type.ComponentForm(request.POST)
        if new_form.is_valid():
            new_component = new_form.save(commit=False)
            new_component.activity = activity
            new_component.id = component.id
            if new_component.position == None:
                count = len(select_all_components(activity))
                new_component.position = count + 1
            new_component.save()
            #LOG EVENT#
            l = LogEntry(userid=request.user.username,
                         description=("edited component %s of %s") %
                         (component.title, activity),
                         related_object=new_component)
            l.save()
            messages.add_message(
                request, messages.SUCCESS, 'Component "' +
                new_component.title + '" successfully updated.')
            return HttpResponseRedirect(
                reverse('offering:submission:show_components',
                        args=[course_slug, activity_slug]))
        else:
            form = new_form
            messages.add_message(request, messages.ERROR,
                                 'Please correct the errors in the form.')

    return render(
        request, "submission/component_edit_single.html", {
            "course": course,
            "activity": activity,
            "component": component,
            "edit_id": edit_id,
            "form": form
        })
예제 #3
0
    def test_select_components(self):
        """
        Test submission component classes: subclasses, selection, sorting.
        """
        _, course = create_offering()
        a1 = NumericActivity(name="Assignment 1", short_name="A1", status="RLS", offering=course, position=2, max_grade=15, due_date="2010-04-01")
        a1.save()
        a2 = NumericActivity(name="Assignment 2", short_name="A2", status="RLS", offering=course, position=1, max_grade=15, due_date="2010-03-01")
        a2.save()

        p = Person.objects.get(userid="ggbaker")
        member = Member(person=p, offering=course, role="INST", career="NONS", added_reason="UNK")
        member.save()

        c1 = URL.Component(activity=a1, title="URL Link", position=8)
        c1.save()
        c2 = Archive.Component(activity=a1, title="Archive File", position=1, max_size=100000)
        c2.save()
        c3 = Code.Component(activity=a1, title="Code File", position=3, max_size=2000, allowed=".py")
        c3.save()
        comps = select_all_components(a1)
        self.assertEqual(len(comps), 3)
        self.assertEqual(comps[0].title, 'Archive File') # make sure position=1 is first
        self.assertEqual(str(comps[1].Type), "submission.models.code.Code")
        self.assertEqual(str(comps[2].Type), "submission.models.url.URL")
예제 #4
0
def show_components_submission_history(request, course_slug, activity_slug, userid=None):
    if userid is None:
        userid = request.GET.get('userid')
    course = get_object_or_404(CourseOffering, slug=course_slug)
    activity = get_object_or_404(course.activity_set,slug = activity_slug, deleted=False)
    staff = False

    if userid is None:
        # can always see your own submissions
        userid = request.user.username
    else:
        # specifying a userid: must be course staff
        if is_course_staff_by_slug(request, course.slug):
            staff = True
        else:
            return ForbiddenResponse(request)

    member = get_object_or_404(Member, find_member(userid), offering=course)
    if activity.group:
        messages.add_message(request, messages.INFO, "This is a group submission. This history is based on submissions from all your group members.")
        gms = GroupMember.objects.filter(student=member, confirmed=True, activity=activity)
        submissions = GroupSubmission.objects.filter(activity=activity, group__groupmember__in=gms)
    else:
        submissions = StudentSubmission.objects.filter(activity=activity, member=member)

    # get all submission components
    component_list = select_all_components(activity, include_deleted=staff)
    all_submitted_components = []
    for submission in submissions:
        c = get_submission_components(submission, activity, component_list)
        all_submitted_components.append({'sub':submission, 'comp':c})
    
    return render(request, "submission/submission_history_view.html",
        {"course":course, "activity":activity,'userid':userid,'submitted_components': all_submitted_components})
예제 #5
0
파일: views.py 프로젝트: avacariu/coursys
def edit_single(request, course_slug, activity_slug):
    course = get_object_or_404(CourseOffering, slug=course_slug)
    activity = get_object_or_404(course.activity_set, slug=activity_slug, deleted=False)
    component_list = select_all_components(activity)

    #get component
    edit_id = request.GET.get('id')
    component = get_component(activity=activity, id=edit_id)
    if component is None:
        return NotFoundResponse(request)

    form = component.Type.ComponentForm(instance=component)
                
    #if form submitted
    if request.method == 'POST':
        new_form = component.Type.ComponentForm(request.POST)
        if new_form.is_valid():
            new_component = new_form.save(commit=False)
            new_component.activity = activity
            new_component.id = component.id
            if new_component.position == None:
                count = len(select_all_components(activity))
                new_component.position = count + 1
            new_component.save()
            #LOG EVENT#
            l = LogEntry(userid=request.user.username,
                  description=(u"edited component %s of %s") % (component.title, activity),
                  related_object=new_component)
            l.save()
            messages.add_message(request, messages.SUCCESS, 'Component "' + new_component.title + '" successfully updated.')
            return HttpResponseRedirect(reverse(show_components, args=[course_slug, activity_slug]))
        else:
            form = new_form
            messages.add_message(request, messages.ERROR, 'Please correct the errors in the form.')

    return render(request, "submission/component_edit_single.html",
            {"course":course, "activity":activity, "component":component, "edit_id":edit_id, "form":form})
예제 #6
0
    def test_select_components(self):
        """
        Test submission component classes: subclasses, selection, sorting.
        """
        _, course = create_offering()
        a1 = NumericActivity(name="Assignment 1",
                             short_name="A1",
                             status="RLS",
                             offering=course,
                             position=2,
                             max_grade=15,
                             due_date="2010-04-01")
        a1.save()
        a2 = NumericActivity(name="Assignment 2",
                             short_name="A2",
                             status="RLS",
                             offering=course,
                             position=1,
                             max_grade=15,
                             due_date="2010-03-01")
        a2.save()

        p = Person.objects.get(userid="ggbaker")
        member = Member(person=p,
                        offering=course,
                        role="INST",
                        career="NONS",
                        added_reason="UNK")
        member.save()

        c1 = URL.Component(activity=a1, title="URL Link", position=8)
        c1.save()
        c2 = Archive.Component(activity=a1,
                               title="Archive File",
                               position=1,
                               max_size=100000)
        c2.save()
        c3 = Code.Component(activity=a1,
                            title="Code File",
                            position=3,
                            max_size=2000,
                            allowed=".py")
        c3.save()
        comps = select_all_components(a1)
        self.assertEqual(len(comps), 3)
        self.assertEqual(comps[0].title,
                         'Archive File')  # make sure position=1 is first
        self.assertEqual(str(comps[1].Type), "submission.models.code.Code")
        self.assertEqual(str(comps[2].Type), "submission.models.url.URL")
예제 #7
0
def copy_setup_activities(course_copy_from, course_copy_to):
        from submission.models.code import CodeComponent
        from submission.models.codefile import CodefileComponent

        # copy Activities (and related content)
        all_activities = all_activities_filter(offering=course_copy_from)

        for activity in all_activities:
            Class = activity.__class__
            new_activity = copy_activity(activity, course_copy_from, course_copy_to)
            save_copied_activity(new_activity, Class, course_copy_to)

            # should only apply to NumericActivity: others have no ActivityComponents
            for activity_component in ActivityComponent.objects.filter(numeric_activity_id=activity.id, deleted=False):
                new_activity_component = copy.deepcopy(activity_component)
                new_activity_component.id = None
                new_activity_component.pk = None
                new_activity_component.numeric_activity = new_activity
                new_activity_component.slug = None
                new_activity_component.save(force_insert=True)
                for common_problem in CommonProblem.objects.filter(activity_component=activity_component, deleted=False):
                    new_common_problem = copy.deepcopy(common_problem)
                    new_common_problem.id = None
                    new_common_problem.pk = None
                    new_common_problem.penalty = str(new_common_problem.penalty)
                    new_common_problem.activity_component = new_activity_component
                    new_common_problem.save(force_insert=True)

            for submission_component in select_all_components(activity):
                new_submission_component = copy.deepcopy(submission_component)
                new_submission_component.id = None
                new_submission_component.pk = None
                new_submission_component.activity = new_activity
                new_submission_component.slug = None
                if isinstance(new_submission_component, CodeComponent):
                    # upgrade Code to Codefile while migrating
                    new_submission_component = CodefileComponent.build_from_codecomponent(new_submission_component)

                # enforce tighter submission size limits
                if hasattr(new_submission_component, 'max_size'):
                    if new_submission_component.max_size > settings.MAX_SUBMISSION_SIZE:
                      new_submission_component.max_size = settings.MAX_SUBMISSION_SIZE

                new_submission_component.save(force_insert=True)

        for activity in CalLetterActivity.objects.filter(offering=course_copy_to):
            # fix up source and exam activities as best possible
            if activity.numeric_activity:
                try:
                    na = NumericActivity.objects.get(offering=course_copy_to, name=activity.numeric_activity.name, deleted=False)
                except NumericActivity.DoesNotExist:
                    na = NumericActivity.objects.filter(offering=course_copy_to, deleted=False)[0]
                activity.numeric_activity = na

            if activity.exam_activity:
                try:
                    a = Activity.objects.get(offering=course_copy_to, name=activity.exam_activity.name, deleted=False)
                except Activity.DoesNotExist:
                    a = Activity.objects.filter(offering=course_copy_to, deleted=False)[0]
                activity.exam_activity = a

            activity.save()
예제 #8
0
def _show_components_student(request, course_slug, activity_slug, userid=None, template="dashboard_student.html", staff=False):
    """
    Show all the component submission history of this activity
    """
    if userid == None:
        userid = request.user.username
    course = get_object_or_404(CourseOffering, slug=course_slug)
    activity = get_object_or_404(course.activity_set,slug=activity_slug, deleted=False)
    student = get_object_or_404(Person, find_userid_or_emplid(userid))
    cansubmit = True

    submission, submitted_components = get_current_submission(student, activity, include_deleted=staff)
    if len(submitted_components) == 0:
        return NotFoundResponse(request)

    if submission and activity.due_date and activity.due_date < submission.created_at:
        late = submission.created_at - activity.due_date
    else:
        late = 0
    
    if activity.group:
        gm = GroupMember.objects.filter(student__person=student, activity=activity, confirmed=True)
        if gm:
            group = gm[0].group
            member = gm[0].student
        else:
            group = None
            #cansubmit = False
            #messages.add_message(request, messages.INFO, "This is a group submission. You cannot submit since you aren't in a group.")
    else:
        group = None

    # activity should be submitable
    cansubmit = cansubmit and activity.submitable()

    if not cansubmit:
        messages.add_message(request, messages.ERROR, "This activity is not submittable.")
        return render(request, "submission/" + template,
        {"course":course, "activity":activity, "submission": submission, "submitted_components":submitted_components,
         "userid":userid, "late":late, "student":student, "group":group, "cansubmit":cansubmit})

    # get all components of activity
    component_list = select_all_components(activity)
    component_list.sort()
    component_form_list=[]

    if request.method == 'POST':
        component_form_list = make_form_from_list(component_list, request=request)
        submitted_comp = []    # list all components which has content submitted in the POST
        not_submitted_comp = [] #list allcomponents which has no content submitted in the POST
        if not activity.group:
            new_sub = StudentSubmission()   # the submission foreign key for newly submitted components
            new_sub.member = get_object_or_404(Member, offering__slug=course_slug, person__userid=request.user.username)
        elif gm:
            new_sub = GroupSubmission()
            new_sub.group = group
            new_sub.creator = member
        else:
            messages.add_message(request, messages.ERROR, "This is a group submission. You cannot submit since you aren't in a group.")
            return ForbiddenResponse(request)
        new_sub.activity = activity

        # begin validating uploaded data
        submitted_comp = []
        not_submitted_comp = []
        # validate forms one by one
        for data in component_form_list:
            component = data['comp']
            form = data['form']
            if form.is_valid():
                sub = form.save(commit=False)
                sub.component = component
                submitted_comp.append(sub)
            else:
                # hack to replace the "required" message to something more appropriate
                for k,v in form.errors.items():
                    for i,e in enumerate(v):
                        if e == "This field is required.":
                            v[i] = "Nothing submitted."

                not_submitted_comp.append(component)
        # check duplicate filenames here
        all_ok = False
        while not all_ok:
            all_ok = True
            d = {}
            for c,s in submitted_components:
                d[c] = s and s.get_filename()
            for s in submitted_comp:
                d[s.component] = s.get_filename()
            # a list holding all file names
            file_name_list = [a[1] for a in d.items() if a[1] is not None]
            to_be_removed = []
            for (i, s) in enumerate(submitted_comp):
                if file_name_list.count(s.get_filename()) > 1:
                    all_ok = False
                    to_be_removed.append(i)
                    not_submitted_comp.append(s.component)
                    #HACK: modify the 'errors' field in the form
                    for data in component_form_list:
                        if s.component == data['comp']:
                            # assume we have only one field for submission form
                            field_name = data['form'].fields.keys()[0]
                            data['form']._errors[field_name] = ErrorList([u"This file has the same name as another file in your submission."])
            # remove those has errors in submitted_comp
            to_be_removed.reverse()
            for t in to_be_removed:
                submitted_comp.pop(t)
        # all okay now
        # end validating, begin saving
        if len(submitted_comp) > 0:
            new_sub.save()    
        for sub in submitted_comp:
            sub.submission = new_sub
            sub.save()
            #LOG EVENT#
            if activity.group:
                group_str = " as a member of group %s" % new_sub.group.name
            else:
                group_str = ""
            l = LogEntry(userid=request.user.username,
                  description=u"submitted for %s %s%s" % (activity, sub.component.title, group_str),
                  related_object=sub)
            l.save()

        if len(not_submitted_comp) == 0:
            messages.add_message(request, messages.SUCCESS, "Your submission was successful.")
            return HttpResponseRedirect(reverse(show_components, args=[course_slug, activity_slug]))

        return render(request, "submission/submission_error.html",
            {"course":course, "activity":activity, "component_list":component_form_list,
            "submitted_comp":submitted_comp, "not_submitted_comp":not_submitted_comp})
    else: #not POST
        if activity.group and gm:
            messages.add_message(request, messages.INFO, "This is a group submission. You will submit on behalf of the group %s." % group.name)
        
        component_form_list = make_form_from_list(component_list)
        return render(request, "submission/" + template,
        {'component_form_list': component_form_list, "course": course, "activity": activity, "submission": submission,
         "submitted_components":submitted_components, "userid":userid, "late":late, "student":student, "group":group,
         "cansubmit":cansubmit, "is_staff":staff})
예제 #9
0
def copyCourseSetup(course_copy_from, course_copy_to):
    """
    copy all the activities setup from one course to another
    copy numeric activities with their marking components, common problems and submission components
    """
    with django.db.transaction.atomic():
        from submission.models.code import CodeComponent
        from submission.models.codefile import CodefileComponent
        # copy things in offering's .config dict
        for f in course_copy_from.copy_config_fields:
            if f in course_copy_from.config:
                course_copy_to.config[f] = course_copy_from.config[f]
        course_copy_to.save()

        # copy Activities (and related content)
        all_activities = all_activities_filter(offering=course_copy_from)

        for activity in all_activities:
            Class = activity.__class__
            new_activity = copy_activity(activity, course_copy_from,
                                         course_copy_to)
            save_copied_activity(new_activity, Class, course_copy_to)

            # should only apply to NumericActivity: others have no ActivityComponents
            for activity_component in ActivityComponent.objects.filter(
                    numeric_activity=activity, deleted=False):
                new_activity_component = copy.deepcopy(activity_component)
                new_activity_component.id = None
                new_activity_component.pk = None
                new_activity_component.numeric_activity = new_activity
                new_activity_component.slug = None
                new_activity_component.save(force_insert=True)
                for common_problem in CommonProblem.objects.filter(
                        activity_component=activity_component, deleted=False):
                    new_common_problem = copy.deepcopy(common_problem)
                    new_common_problem.id = None
                    new_common_problem.pk = None
                    new_common_problem.penalty = str(
                        new_common_problem.penalty)
                    new_common_problem.activity_component = new_activity_component
                    new_common_problem.save(force_insert=True)

            for submission_component in select_all_components(activity):
                new_submission_component = copy.deepcopy(submission_component)
                new_submission_component.id = None
                new_submission_component.pk = None
                new_submission_component.activity = new_activity
                new_submission_component.slug = None
                if isinstance(new_submission_component, CodeComponent):
                    # upgrade Code to Codefile while migrating
                    new_submission_component = CodefileComponent.build_from_codecomponent(
                        new_submission_component)

                # enforce tighter submission size limits
                if hasattr(new_submission_component, 'max_size'):
                    if new_submission_component.max_size > settings.MAX_SUBMISSION_SIZE:
                        new_submission_component.max_size = settings.MAX_SUBMISSION_SIZE

                new_submission_component.save(force_insert=True)

        for activity in CalLetterActivity.objects.filter(
                offering=course_copy_to):
            # fix up source and exam activities as best possible
            if activity.numeric_activity:
                try:
                    na = NumericActivity.objects.get(
                        offering=course_copy_to,
                        name=activity.numeric_activity.name,
                        deleted=False)
                except NumericActivity.DoesNotExist:
                    na = NumericActivity.objects.filter(
                        offering=course_copy_to, deleted=False)[0]
                activity.numeric_activity = na

            if activity.exam_activity:
                try:
                    a = Activity.objects.get(offering=course_copy_to,
                                             name=activity.exam_activity.name,
                                             deleted=False)
                except Activity.DoesNotExist:
                    a = Activity.objects.filter(offering=course_copy_to,
                                                deleted=False)[0]
                activity.exam_activity = a

            activity.save()

        # copy the Pages
        from pages.models import Page, PageFilesStorage, attachment_upload_to
        for p in Page.objects.filter(offering=course_copy_from):
            new_p = copy.deepcopy(p)
            new_p.id = None
            new_p.pk = None
            new_p.offering = course_copy_to
            while True:
                count = 0
                orig_label = new_p.label
                try:
                    new_p.save(force_insert=True)
                    break
                except IntegrityError:
                    count += 1
                    new_p.label = orig_label + "-" + str(count)

            # if there are release dates, adapt to new semester
            if new_p.releasedate():
                week, wkday = course_copy_from.semester.week_weekday(
                    new_p.releasedate())
                new_date = course_copy_to.semester.duedate(week, wkday, None)
                new_p.set_releasedate(new_date)
            if new_p.editdate():
                week, wkday = course_copy_from.semester.week_weekday(
                    new_p.editdate())
                new_date = course_copy_to.semester.duedate(week, wkday, None)
                new_p.set_editdate(new_date)

            new_p.save()

            v = p.current_version()
            new_v = copy.deepcopy(v)
            new_v.id = None
            new_v.pk = None
            new_v.page = new_p
            # collapse old version history
            new_v.wikitext = v.get_wikitext()
            new_v.diff = None
            new_v.diff_from = None
            new_v.comment = "Page migrated from %s" % (course_copy_from)

            if new_v.file_attachment:
                # copy the file (so we can safely remove old semesters'
                # files without leaving bad path reference)
                src = v.file_attachment.path
                path = attachment_upload_to(new_v, new_v.file_name)
                dst = PageFilesStorage.path(path)
                dstpath, dstfile = os.path.split(dst)
                while os.path.exists(os.path.join(dstpath, dstfile)):
                    # handle duplicates by mangling the directory name
                    dstpath += "_"
                dst = os.path.join(dstpath, dstfile)
                new_v.file_attachment = dst

                if not os.path.exists(dstpath):
                    os.makedirs(dstpath)

                try:
                    os.link(src, dst)
                except:
                    # any problems with the hardlink: try simple copy
                    import shutil
                    shutil.copyfile(src, dst)

            new_v.save(force_insert=True)
예제 #10
0
def copyCourseSetup(course_copy_from, course_copy_to):
    """
    copy all the activities setup from one course to another
    copy numeric activities with their marking components, common problems and submission components
    """
    with django.db.transaction.atomic():
        from submission.models.code import CodeComponent
        from submission.models.codefile import CodefileComponent
        # copy things in offering's .config dict
        for f in course_copy_from.copy_config_fields:
            if f in course_copy_from.config:
                course_copy_to.config[f] = course_copy_from.config[f]
        course_copy_to.save()

        # copy Activities (and related content)
        all_activities = all_activities_filter(offering=course_copy_from)

        for activity in all_activities:
            Class = activity.__class__
            new_activity = copy_activity(activity, course_copy_from, course_copy_to)
            save_copied_activity(new_activity, Class, course_copy_to)
        
            # should only apply to NumericActivity: others have no ActivityComponents
            for activity_component in ActivityComponent.objects.filter(numeric_activity=activity, deleted=False):
                new_activity_component = copy.deepcopy(activity_component)
                new_activity_component.id = None
                new_activity_component.pk = None
                new_activity_component.numeric_activity = new_activity
                new_activity_component.slug = None
                new_activity_component.save(force_insert=True)
                for common_problem in CommonProblem.objects.filter(activity_component=activity_component, deleted=False):
                    new_common_problem = copy.deepcopy(common_problem)
                    new_common_problem.id = None
                    new_common_problem.pk = None
                    new_common_problem.penalty = str(new_common_problem.penalty)
                    new_common_problem.activity_component = new_activity_component
                    new_common_problem.save(force_insert=True)
            
            for submission_component in select_all_components(activity):
                new_submission_component = copy.deepcopy(submission_component)
                new_submission_component.id = None
                new_submission_component.pk = None
                new_submission_component.activity = new_activity
                new_submission_component.slug = None
                if isinstance(new_submission_component, CodeComponent):
                    # upgrade Code to Codefile while migrating
                    new_submission_component = CodefileComponent.build_from_codecomponent(new_submission_component)
                
                # enforce tighter submission size limits
                if hasattr(new_submission_component, 'max_size'):
                    if new_submission_component.max_size > settings.MAX_SUBMISSION_SIZE:
                      new_submission_component.max_size = settings.MAX_SUBMISSION_SIZE
                
                new_submission_component.save(force_insert=True)
        
        for activity in CalLetterActivity.objects.filter(offering=course_copy_to):
            # fix up source and exam activities as best possible
            if activity.numeric_activity:
                try:
                    na = NumericActivity.objects.get(offering=course_copy_to, name=activity.numeric_activity.name, deleted=False)
                except NumericActivity.DoesNotExist:
                    na = NumericActivity.objects.filter(offering=course_copy_to, deleted=False)[0]
                activity.numeric_activity = na
                
            if activity.exam_activity:
                try:
                    a = Activity.objects.get(offering=course_copy_to, name=activity.exam_activity.name, deleted=False)
                except Activity.DoesNotExist:
                    a = Activity.objects.filter(offering=course_copy_to, deleted=False)[0]
                activity.exam_activity = a
            
            activity.save()
        
        
        # copy the Pages
        from pages.models import Page, PageFilesStorage, attachment_upload_to
        for p in Page.objects.filter(offering=course_copy_from):
            new_p = copy.deepcopy(p)
            new_p.id = None
            new_p.pk = None
            new_p.offering = course_copy_to
            while True:
                count = 0
                orig_label = new_p.label
                try:
                    new_p.save(force_insert=True)
                    break
                except IntegrityError:
                    count += 1
                    new_p.label = orig_label + "-" + str(count)
            
            # if there are release dates, adapt to new semester
            if new_p.releasedate():
                week, wkday = course_copy_from.semester.week_weekday(new_p.releasedate())
                new_date = course_copy_to.semester.duedate(week, wkday, None)
                new_p.set_releasedate(new_date)
            if new_p.editdate():
                week, wkday = course_copy_from.semester.week_weekday(new_p.editdate())
                new_date = course_copy_to.semester.duedate(week, wkday, None)
                new_p.set_editdate(new_date)

            new_p.save()

            v = p.current_version()
            new_v = copy.deepcopy(v)
            new_v.id = None
            new_v.pk = None
            new_v.page = new_p
            # collapse old version history
            new_v.wikitext = v.get_wikitext()
            new_v.diff = None
            new_v.diff_from = None
            new_v.comment = "Page migrated from %s" % (course_copy_from)
            
            if new_v.file_attachment:
                # copy the file (so we can safely remove old semesters'
                # files without leaving bad path reference)
                src = v.file_attachment.path
                path = attachment_upload_to(new_v, new_v.file_name)
                dst = PageFilesStorage.path(path)
                dstpath, dstfile = os.path.split(dst)
                while os.path.exists(os.path.join(dstpath, dstfile)):
                    # handle duplicates by mangling the directory name
                    dstpath += "_"
                dst = os.path.join(dstpath, dstfile)
                new_v.file_attachment = dst
                
                if not os.path.exists(dstpath):
                    os.makedirs(dstpath)

                try:
                    os.link(src, dst)
                except:
                    # any problems with the hardlink: try simple copy
                    import shutil
                    shutil.copyfile(src, dst)

            new_v.save(force_insert=True)