--- loncom/homework/grades.pm 2003/04/01 05:21:48 1.82 +++ loncom/homework/grades.pm 2003/05/30 21:42:36 1.93 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.82 2003/04/01 05:21:48 albertel Exp $ +# $Id: grades.pm,v 1.93 2003/05/30 21:42:36 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -46,6 +46,9 @@ use Apache::lonhomework; use Apache::loncoursedata; use Apache::lonmsg qw(:user_normal_msg); use Apache::Constants qw(:common); +use String::Similarity; + +my %oldessays=(); # ----- These first few routines are general use routines.---- # @@ -221,6 +224,50 @@ sub jscriptNform { } #------------------ End of general use routines -------------------- + +# +# Find most similar essay +# + +sub most_similar { + my ($uname,$udom,$uessay)=@_; + +# ignore spaces and punctuation + + $uessay=~s/\W+/ /gs; + +# these will be returned. Do not care if not at least 50 percent similar + my $limit=0.6; + my $sname=''; + my $sdom=''; + my $scrsid=''; + my $sessay=''; +# go through all essays ... + foreach my $tkey (keys %oldessays) { + my ($tname,$tdom,$tcrsid)=split(/\./,$tkey); +# ... except the same student + if (($tname ne $uname) || ($tdom ne $udom)) { + my $tessay=$oldessays{$tkey}; + $tessay=~s/\W+/ /gs; +# String similarity gives up if not even limit + my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit); +# Found one + if ($tsimilar>$limit) { + $limit=$tsimilar; + $sname=$tname; + $sdom=$tdom; + $scrsid=$tcrsid; + $sessay=$oldessays{$tkey}; + } + } + } + if ($limit>0.6) { + return ($sname,$sdom,$scrsid,$sessay,$limit); + } else { + return ('','','','',0); + } +} + #------------------------------------------------------------------- #------------------------------------ Receipt Verification Routines @@ -717,7 +764,7 @@ sub sub_page_kw_js { height = 600; scrollbar = "yes"; } -// if (window.pWin) window.pWin.close(); +// if (window.pWin) {window.pWin.close(); window.pWin=null} pWin = window.open('', 'MessageCenter', 'toolbar=no,location=no,scrollbars='+scrollbar+',screenx=70,screeny=75,width=600,height='+height); pWin.focus(); pDoc = pWin.document; @@ -752,11 +799,7 @@ sub sub_page_kw_js { pDoc.write(" includemsg = 1;"); pDoc.write(" }"); pDoc.write(" imgformname = eval(\\"opener.document.SCORE.mailicon\\"+usrctr);"); - pDoc.write(" if (includemsg) {"); - pDoc.write(" imgformname.src = \\"$iconpath/mailto.gif\\";"); - pDoc.write(" } else {"); - pDoc.write(" imgformname.src = \\"$iconpath/mailbkgrd.gif\\";"); - pDoc.write(" }"); + pDoc.write(" imgformname.src = \\"$iconpath/\\"+((includemsg) ? \\"mailto.gif\\" : \\"mailbkgrd.gif\\");"); pDoc.write(" var includemsg = eval(\\"opener.document.SCORE.includemsg\\"+usrctr);"); pDoc.write(" includemsg.value = msgchk;"); @@ -1079,6 +1122,9 @@ sub submission { $request->print($prnmsg); if ($ENV{'form.handgrade'} eq 'yes' && $ENV{'form.showgrading'} eq 'yes') { +# +# Print out the keyword options line +# $request->print(<Keyword Options:  List    @@ -1086,6 +1132,14 @@ sub submission { CLASS="page">Paste Selection to List    Highlight Attribute

KEYWORDS +# +# Load the other essays for similarity check +# + my $essayurl=&Apache::lonnet::declutter($url); + my ($adom,$aname,$apath)=($essayurl=~/^(\w+)\/(\w+)\/(.*)$/); + $apath=&Apache::lonnet::escape($apath); + $apath=~s/\W/\_/gs; + %oldessays=&Apache::lonnet::dump('nohist_essay_'.$apath,$adom,$aname); } } @@ -1112,7 +1166,6 @@ KEYWORDS my @col_fullnames; my ($classlist,$fullname); if ($ENV{'form.handgrade'} eq 'yes') { - my @col_list; ($classlist,undef,$fullname) = &getclasslist('all','0'); for (keys (%$handgrade)) { my $ncol = &Apache::lonnet::EXT('resource.'.$_. @@ -1121,56 +1174,46 @@ KEYWORDS next if ($ncol <= 0); s/\_/\./g; next if ($record{'resource.'.$_.'.collaborators'} eq ''); - my (@colList) = split(/,?\s+/, - $record{'resource.'.$_.'.collaborators'}); - my @collaborators = (); - foreach (@colList) { #pre-filter list - throw out submitter + my @goodcollaborators = (); + my @badcollaborators = (); + foreach (split(/,?\s+/,$record{'resource.'.$_.'.collaborators'})) { + $_ =~ s/[\$\^\(\)]//g; + next if ($_ eq ''); my ($co_name,$co_dom) = split /\@|:/,$_; - $co_dom = $udom if (! defined($co_dom)); + $co_dom = $udom if (! defined($co_dom) || $co_dom =~ /^domain$/i); next if ($co_name eq $uname && $co_dom eq $udom); - push @collaborators, $_; + # Doing this grep allows 'fuzzy' specification + my @Matches = grep /^$co_name:$co_dom$/i,keys %$classlist; + if (! scalar(@Matches)) { + push @badcollaborators,$_; + } else { + push @goodcollaborators, @Matches; + } } - my (@badcollaborators); - if (scalar(@collaborators) != 0) { + if (scalar(@goodcollaborators) != 0) { $result.='Collaborators: '; - foreach my $collaborator (@collaborators) { - my ($co_name,$co_dom) = split /\@|:/,$collaborator; - $co_dom = $udom if (! defined($co_dom)); - # Doing this grep allows 'fuzzy' specification - my @Matches = grep /^$co_name:$co_dom$/i, - keys %$classlist; - if (! scalar(@Matches)) { - push @badcollaborators,':'.$collaborator.':'; - next; - } - push @col_list, @Matches; - foreach (@Matches) { - my ($lastname,$givenn) = split(/,/,$$fullname{$_}); - push @col_fullnames, $givenn.' '.$lastname; - $result.=$$fullname{$_}.'     '; - } - } + foreach (@goodcollaborators) { + my ($lastname,$givenn) = split(/,/,$$fullname{$_}); + push @col_fullnames, $givenn.' '.$lastname; + $result.=$$fullname{$_}.'     '; + } $result.='
'."\n"; - if (scalar(@badcollaborators) > 0) { - $result.='
'; - $result.='This student has submitted '; - if (scalar(@badcollaborators) == 1) { - $result .= 'an invalid collaborator'; - } else { - $result .= 'invalid collaborators'; - } - $result .= ': '.join(', ',@badcollaborators); - $result .= '
'; - } - if (scalar(@collaborators > $ncol)) { - $result .= '
'; - $result .= 'This student has submitted too many '. - 'collaborators. Maximum is '.$ncol; - $result .= '
'; - } - $result.=''."\n"; - } + $result.=''."\n"; + } + if (scalar(@badcollaborators) > 0) { + $result.='
'; + $result.='This student has submitted '; + $result.=(scalar(@badcollaborators) == 1) ? 'an invalid collaborator' : 'invalid collaborators'; + $result .= ': '.join(', ',@badcollaborators); + $result .= '
'; + } + if (scalar(@badcollaborators > $ncol)) { + $result .= '
'; + $result .= 'This student has submitted too many '. + 'collaborators. Maximum is '.$ncol.'.'; + $result .= '
'; + } } } $request->print($result."\n"); @@ -1204,6 +1247,15 @@ KEYWORDS my ($partid,$respid) = /^resource\.(\d+)\.(\d+)\.submission/; if ($part eq ($partid.'_'.$respid)) { my ($ressub,$subval) = split(/:/,$_,2); +# Similarity check + my $similar=''; + my ($oname,$odom,$ocrsid,$oessay,$osim)=&most_similar($uname,$udom,$subval); + if ($osim) { + $osim=int($osim*100.0); + $similar='

Essay is '.$osim.'% similar to an essay by '.&Apache::loncommon::plainname($oname,$odom). + '

'. + &keywords_highlight($oessay).'

'; + } $lastsubonly.='Part '. $partid.' ( ID '.$respid. ' )   '. @@ -1211,8 +1263,8 @@ KEYWORDS ' File uploaded by student Like all files provided by users, this file may contain virusses
':''). - 'Answer: '. - &keywords_highlight($subval).''."\n" + 'Answer:
'. + &keywords_highlight($subval).'

 '.$similar.''."\n" if ($ENV{'form.lastSub'} eq 'lastonly' || ($ENV{'form.lastSub'} eq 'hdgrade' && $$handgrade{$part} =~ /:yes$/)); @@ -1250,6 +1302,7 @@ KEYWORDS my $lastone = pop @col_fullnames; $msgfor .= ', '.(join ', ',@col_fullnames).' and '.$lastone.'.'; } + $msgfor =~ s/\'/\\'/g; #' stupid emacs $result.=''."\n". ' '. @@ -1447,12 +1500,20 @@ sub processHandGrade { $ENV{'course.'.$ENV{'request.course.id'}.'.num'}); # Called by Save & Refresh from Highlight Attribute Window + my (undef,undef,$fullname) = &getclasslist($ENV{'form.section'},'0'); if ($ENV{'form.refresh'} eq 'on') { - my $ctr = 0; - $ENV{'form.NTSTU'}=$ngrade; + my ($ctr,$total) = (0,0); while ($ctr < $ngrade) { - ($ENV{'form.student'},$ENV{'form.userdom'}) = split(/:/,$ENV{'form.unamedom'.$ctr}); - &submission($request,$ctr,$ngrade-1); + $total++ if $ENV{'form.unamedom'.$ctr} ne ''; + $ctr++; + } + $ENV{'form.NTSTU'}=$ngrade; + $ctr = 0; + while ($ctr < $total) { + my $processUser = $ENV{'form.unamedom'.$ctr}; + ($ENV{'form.student'},$ENV{'form.userdom'}) = split(/:/,$processUser); + $ENV{'form.fullname'} = $$fullname{$processUser}; + &submission($request,$ctr,$total-1); $ctr++; } return ''; @@ -1468,7 +1529,6 @@ sub processHandGrade { $laststu = $firststu if ($ctr > $ngrade); } - my (undef,undef,$fullname) = &getclasslist($ENV{'form.section'},'0'); my (@parsedlist,@nextlist); my ($nextflg) = 0; foreach (sort {lc($$fullname{$a}) cmp lc($$fullname{$b}) } keys %$fullname) { @@ -1853,7 +1913,8 @@ sub viewgrades { my (undef,undef,$fullname) = &getclasslist($ENV{'form.section'},'1'); my $ctr = 0; foreach (sort {lc($$fullname{$a}) cmp lc($$fullname{$b}) } keys %$fullname) { - my ($uname,$udom) = split(/:/); + my $uname = $_; + $uname=~s/:/_/; $result.=''."\n"; $result.=&viewstudentgrade($url,$symb,$ENV{'request.course.id'}, $_,$$fullname{$_},\@parts,\%weight); @@ -1871,6 +1932,7 @@ sub viewgrades { sub viewstudentgrade { my ($url,$symb,$courseid,$student,$fullname,$parts,$weight) = @_; my ($uname,$udom) = split(/:/,$student); + $student=~s/:/_/; my %record=&Apache::lonnet::restore($symb,$courseid,$udom,$uname); my $result=''. ''."\n"; + 'GD_'.$student.'_'.$part.'_awarded_s" value="'.$pts.'" />'."\n"; $result.=''."\n"; } elsif ($type eq 'solved') { my ($status,$foo)=split(/_/,$score,2); $status = 'nothing' if ($status eq ''); - $result.=''."\n"; $result.='\n"; } else { $result.=''. + 'GD_'.$student.'_'.$part.'_'.$type.'_s" value="'.$score.'" />'. "\n"; $result.=''."\n"; } } @@ -1925,7 +1987,7 @@ sub editgrades { $title.='Section: '.$ENV{'form.section'}.''."\n"; my $result= '
'."\n"; $result.= ''. - ''."\n"; + ''."\n"; my %scoreptr = ( 'correct' =>'correct_by_override', @@ -1973,16 +2035,19 @@ sub editgrades { $result .= ''; $result .= $header; $result .= ''."\n"; - + my $noupdate; for ($i=0; $i<$ENV{'form.total'}; $i++) { + my $line; my $user = $ENV{'form.ctr'.$i}; + my $usercolon = $user; + $usercolon =~s/_/:/; + my ($uname,$udom)=split(/_/,$user); my %newrecord; my $updateflag = 0; - my @userdom = grep /^$user:/,keys %$classlist; - my (undef,$udom) = split(/:/,$userdom[0]); - $result .= ''; + $line .= ''; foreach (@partid) { my $old_aw = $ENV{'form.GD_'.$user.'_'.$_.'_awarded_s'}; my $old_part_pcr = $old_aw/($weight{$_} ne '0' ? $weight{$_}:1); @@ -2002,7 +2067,7 @@ sub editgrades { } $score = 'excused' if (($ENV{'form.GD_'.$user.'_'.$_.'_solved'} eq 'excused') && ($score ne 'excused')); - $result .= ''. + $line .= ''. ''; @@ -2025,17 +2090,23 @@ sub editgrades { $newrecord{'resource.'.$part.'regrader'}="$ENV{'user.name'}:$ENV{'user.domain'}"; $updateflag=1; } - $result .= ''. + $line .= ''. ''; } } - $result .= ''."\n"; + $line.=''."\n"; if ($updateflag) { $count++; &Apache::lonnet::cstore(\%newrecord,$symb,$ENV{'request.course.id'}, - $udom,$user); + $udom,$uname); + $result.=$line; + } else { + $noupdate.=$line; } } + if ($noupdate) { + $result .= ''.$noupdate; + } $result .= '
UsernameFullnameUsernameDomainFullname
'.$user.' '. - $$fullname{$userdom[0]}.' 
'.$uname.' '. + $udom.' '. + $$fullname{$usercolon}.' '.$old_aw.' '.$old_aw.' '.$awarded. ($score eq 'excused' ? $score : '').' '.$old_aw.' '.$old_aw.' '.$awarded.' 
No Changes Occured For the Students Below
'."\n". &show_grading_menu_form ($symb,$url); my $msg = 'Number of records updated = '.$rec_update. @@ -2214,6 +2285,48 @@ sub csvuploadmap_footer { ENDPICK } +sub upcsvScores_form { + my ($request) = shift; + my ($symb,$url)=&get_symb_and_url($request); + if (!$symb) {return '';} + my $result =< + function checkUpload(formname) { + if (formname.upfile.value == "") { + alert("Please use the browse button to select a file from your local directory."); + return false; + } + formname.submit(); + } + +CSVFORMJS + $ENV{'form.probTitle'} = &Apache::lonnet::gettitle($symb); + $result.='
'."\n"; + $result.=''."\n"; + $result.='
'."\n"; + $result.=' Specify a file containing the class scores for problem - '.$ENV{'form.probTitle'}. + '.
'."\n"; + my $upfile_select=&Apache::loncommon::upfile_select_html(); + $result.=< + + + + + +$upfile_select +
+ + +ENDUPFORM + $result.='
'."\n"; + $result.='


'."\n"; + $result.=&show_grading_menu_form($symb,$url); + + return $result; +} + + sub csvuploadmap { my ($request)= @_; my ($symb,$url)=&get_symb_and_url($request); @@ -2556,7 +2669,6 @@ sub displayPage { if (ref($curRes) && $curRes->is_problem() && !$curRes->randomout) { my $parts = $curRes->parts(); - $parts = &temp_parts_fix($parts); # remove line when lonnavmap is fixed my $title = $curRes->compTitle(); my $symbx = $curRes->symb(); $studentTable.=''.$question. @@ -2649,18 +2761,6 @@ sub displayPage { return ''; } -sub temp_parts_fix { #remove sub once lonnavmap is fixed - my $parts = shift; - my %seen = (); - my @correctParts = (); - foreach (@{$parts}) { - next if ($seen{$_} > 0); - $seen{$_}++; - push @correctParts,$_; - } - return \@correctParts; -} - sub updateGradeByPage { my ($request) = shift; @@ -2701,7 +2801,6 @@ sub updateGradeByPage { if (ref($curRes) && $curRes->is_problem() && !$curRes->randomout) { my $parts = $curRes->parts(); - $parts = &temp_parts_fix($parts); # remove line when lonnavmap is fixed my $title = $curRes->compTitle(); my $symbx = $curRes->symb(); $studentTable.=''.$question. @@ -2924,47 +3023,56 @@ sub scantron_parse_scanline { my $data=substr($line,0,$$scantron_config{'Qstart'}-1); if ($$scantron_config{'CODElocation'} ne 0) { if ($$scantron_config{'CODElocation'} < 0) { - $record{'CODE'}=substr($data,$$scantron_config{'CODEstart'}-1, - $$scantron_config{'CODElength'}); + $record{'scantron.CODE'}=substr($data,$$scantron_config{'CODEstart'}-1, + $$scantron_config{'CODElength'}); } else { #FIXME interpret first N questions } } - $record{'ID'}=substr($data,$$scantron_config{'IDstart'}-1, - $$scantron_config{'IDlength'}); + $record{'scantron.ID'}=substr($data,$$scantron_config{'IDstart'}-1, + $$scantron_config{'IDlength'}); my @alphabet=('A'..'Z'); my $questnum=0; while ($questions) { $questnum++; my $currentquest=substr($questions,0,$$scantron_config{'Qlength'}); substr($questions,0,$$scantron_config{'Qlength'})=''; + if (length($currentquest) < $$scantron_config{'Qlength'}) { next; } my (@array)=split(/$$scantron_config{'Qon'}/,$currentquest); if (scalar(@array) gt 2) { #FIXME do something intelligent with double bubbles - Apache->request->print("
Wha!!! ".scalar(@array). - '-'.$questions.'-'.$currentquest.'-'.$questnum. - '-'.length($questions). - '-'.$line.'-'.length($line).'-'. - '-'.$data.'-'.length($data).'-'. - '
'); + Apache->request->print("
Wha!!!
".scalar(@array).
+				   '-'.$currentquest.'-'.$questnum.'

'); } if (length($array[0]) eq $$scantron_config{'Qlength'}) { - $record{"$questnum.answer"}=''; + $record{"scantron.$questnum.answer"}=''; } else { - $record{"$questnum.answer"}=$alphabet[length($array[0])]; + $record{"scantron.$questnum.answer"}=$alphabet[length($array[0])]; } } - $record{'maxquest'}=$questnum; - $Apache::lonxml::debug=1; - &Apache::lonhomework::showhash(%record); - $Apache::lonxml::debug=0; - return %record; + $record{'scantron.maxquest'}=$questnum; + return \%record; } sub scantron_add_delay { } sub scantron_find_student { + my ($scantron_record,$idmap)=@_; + my $scanID=$$scantron_record{'scantron.ID'}; + foreach my $id (keys(%$idmap)) { + Apache->request->print('
checking studnet -'.$id.'- againt -'.$scanID.'- 
'); + if (lc($id) eq lc($scanID)) { Apache->request->print('success');return $$idmap{$id}; } + } + return undef; +} + +sub scantron_filter { + my ($curres)=@_; + if (ref($curres) && $curres->is_problem() && !$curres->randomout) { + return 1; + } + return 0; } sub scantron_process_students { @@ -2976,8 +3084,13 @@ sub scantron_process_students { my %scantron_config=&get_scantron_config($ENV{'form.scantron_format'}); my $scanlines=Apache::File->new($Apache::lonnet::perlvar{'lonScansDir'}."/$ENV{'form.scantron_selectfile'}"); + my @scanlines=<$scanlines>; my $classlist=&Apache::loncoursedata::get_classlist(); my %idmap=&username_to_idmap($classlist); + my $navmap=Apache::lonnavmaps::navmap->new($ENV{'request.course.fn'}.'.db',$ENV{'request.course.fn'}.'_parms.db',1, 1); + my $map=$navmap->getResourceByUrl($sequence); + my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0); + $r->print("geto ".scalar(@resources)."
"); my $result= < @@ -2986,15 +3099,65 @@ SCANTRONFORM $r->print($result); my @delayqueue; + my $totalcorrect; + my $totalincorrect; + + my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin($r, + 'Scantron Status','Scantron Progress',scalar(@scanlines)); + foreach my $line (@scanlines) { + my $studentcorrect; + my $studentincorrect; - foreach my $line (<$scanlines>) { + chomp($line); my $scan_record=&scantron_parse_scanline($line,\%scantron_config); my ($uname,$udom); if ($uname=&scantron_find_student($scan_record,\%idmap)) { &scantron_add_delay(\@delayqueue,$line, 'Unable to find a student that matches'); } + $r->print('
doing studnet'.$uname.'
'); ($uname,$udom)=split(/:/,$uname); + &Apache::lonnet::delenv('form.counter'); + &Apache::lonnet::appenv(%$scan_record); +# &Apache::lonhomework::showhash(%ENV); + $Apache::lonxml::debug=1; + &Apache::lonxml::debug("line is $line"); + + my $i=0; + foreach my $resource (@resources) { + $i++; + my $result=&Apache::lonnet::ssi($resource->src(), + ('submitted' =>'scantron', + 'grade_target' =>'grade', + 'grade_username'=>$uname, + 'grade_domain' =>$udom, + 'grade_courseid'=>$ENV{'request.course.id'}, + 'grade_symb' =>$resource->symb())); + my %score=&Apache::lonnet::restore($resource->symb(), + $ENV{'request.course.id'}, + $udom,$uname); + foreach my $part ($resource->{PARTS}) { + if ($score{'resource.'.$part.'.solved'} =~ /^correct/) { + $studentcorrect++; + $totalcorrect++; + } else { + $studentincorrect++; + $totalincorrect++; + } + } + $r->print('
'.
+		      $resource->symb().'-'.
+		      $resource->src().'-'.'
result is'.$result); + &Apache::lonhomework::showhash(%score); + # if ($i eq 3) {last;} + } + &Apache::lonnet::delenv('form.counter'); + &Apache::lonnet::delenv('scantron\.'); + &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state, + 'last student Who got a '.$studentcorrect.' correct and '. + $studentincorrect.' incorrect. The class has gotten '. + $totalcorrect.' correct and '.$totalincorrect.' incorrect'); + last; #FIXME #get iterator for $sequence #foreach question 'submit' the students answer to the server @@ -3002,6 +3165,7 @@ SCANTRONFORM # generate data to pass back that includes grade recevied #} } + $Apache::lonxml::debug=0; foreach my $delay (@delayqueue) { #FIXME #print out each delayed student with interface to select how @@ -3014,7 +3178,8 @@ SCANTRONFORM #FIXME # if delay queue exists 2 submits one to process delayed students one # to ignore delayed students, possibly saving the delay queue for later - + + $navmap->untieHashes(); } #-------- end of section for handling grading scantron forms ------- # @@ -3061,10 +3226,7 @@ sub gradingmenu { var cmd = formname.command; formname.saveState.value = "saveCmd="+radioSelection(cmd)+":saveSec="+pullDownSelection(formname.section)+ ":saveSub="+radioSelection(formname.submitonly)+":saveStatus="+pullDownSelection(formname.status); - if (cmd[0].checked || cmd[1].checked || cmd[2].checked || cmd[4].checked) formname.submit(); - - if (cmd[3].checked) browseAndUpload(); - + if (cmd[0].checked || cmd[1].checked || cmd[2].checked || cmd[3].checked || cmd[4].checked) formname.submit(); if (cmd[5].checked) { if (!checkReceiptNo(formname,'notOK')) { return false;} formname.submit(); @@ -3113,57 +3275,6 @@ sub gradingmenu { } } - function browseAndUpload() { - bNLoad = window.open('', 'BrowseAndUpload', 'toolbar=no,location=no,scrollbars=no,width=550,height=200,screenx=100,screeny=75'); - bNLoad.focus(); - var lDoc = bNLoad.document; - lDoc.write(""); - lDoc.write("Browse And Upload"); - - lDoc.write(" GRADINGMENUJS @@ -3225,8 +3336,8 @@ GRADINGMENUJS ($saveSub eq 'all' ? 'checked' : '').' /> everybody'."\n". ''. - ' '. + ' '. 'Upload scores from file'."\n"; $result.=''.