Documentos (CV) de los candidatos (a falta de repaso)

git-svn-id: https://192.168.0.254/svn/Proyectos.Incam_PROFIND_Web/trunk@74 3fe1ab16-cfe0-e34b-8c9f-7d8c168d430d
This commit is contained in:
David Arranz 2012-12-04 17:42:23 +00:00
parent 40dc4031b5
commit b4eedbc7ee
9 changed files with 670 additions and 3 deletions

View File

@ -180,6 +180,7 @@ class CandidatoController extends Controller {
throw new CHttpException(405, Yii::t('profind', 'Método no permitido.'));
}
Yii::trace(CVarDumper::dumpAsString($_POST));
Yii::trace(CVarDumper::dumpAsString($_FILES));
// Candidato
$candidato->attributes = $_POST['Candidato'];
@ -239,6 +240,29 @@ class CandidatoController extends Controller {
$candidato->idiomas = $listaIdiomas;
}
// Documentos del candidato
$listaDocumentosBorrar = array();
if (isset($_POST['CandidatoDocumento'])) {
$listaDocumentos = array();
foreach ($_POST['CandidatoDocumento'] as $key => $documento) {
if ($documento['id'])
$candidatoDocumento = CandidatoDocumento::model()->findByPk($documento['id']);
else {
$candidatoDocumento = new CandidatoDocumento();
$candidatoDocumento->ficheroDocumento = CUploadedFile::getInstance($candidatoDocumento, "[$key]nombre_fichero");
Yii::trace(CVarDumper::dumpAsString($candidatoDocumento->ficheroDocumento));
}
$candidatoDocumento->attributes = $documento;
$candidatoDocumento->id_candidato = $candidato->id;
if ($documento['_borrar'])
$listaDocumentosBorrar[] = $candidatoDocumento;
else
$listaDocumentos[] = $candidatoDocumento;
}
$candidato->documentos = $listaDocumentos;
}
// Guardar los datos
$errores = array();
$transaccion = Yii::app()->db->beginTransaction();
@ -326,6 +350,30 @@ class CandidatoController extends Controller {
}
}
// Documentos
if (!empty($listaDocumentosBorrar)) {
Yii::trace('Eliminando documentos marcados para borrar', 'application.controllers.CandidatoController');
foreach ($listaDocumentosBorrar as $candidatoDocumento) {
Yii::trace('Eliminando documento... ', 'application.controllers.CandidatoController');
Yii::trace(CVarDumper::dumpAsString($candidatoDocumento->attributes), 'application.controllers.CandidatoController');
if (!$candidatoDocumento->delete()) {
$errores = array_merge($errores, $candidatoDocumento->getErrors());
throw new CException('Error al eliminar un documento del candidato');
}
}
$listaDocumentosBorrar = NULL;
}
if (!empty($candidato->documentos)) {
Yii::trace('Guardando la lista de documentos', 'application.controllers.CandidatoController');
foreach ($candidato->documentos as $candidatoDocumento) {
Yii::trace(CVarDumper::dumpAsString($candidatoDocumento->attributes), 'application.controllers.CandidatoController');
if (!$candidatoDocumento->save()) {
$errores = array_merge($errores, $candidatoDocumento->getErrors());
throw new CException('Error al guardar un documento del candidato');
}
}
}
$transaccion->commit();
Yii::trace('Candidato guardado', 'application.controllers.CandidatoController');
Yii::app()->user->setFlash('success', Yii::t('profind', 'Se ha actualizado el candidato'));

View File

@ -0,0 +1,46 @@
<?php
/**
* FileHelper holds a collection of static methods, useful for generic purposes
*
*/
class FileHelper {
/**
* Returns a safe filename by replacing all dangerous characters
* with an underscore.
*
* @param string $filename The source filename to be "sanitized"
*
* @return Boolean string A safe version of the input filename
*/
public static function sanitizeFileName($filename) {
// replace non letter or digits by -
$filename = preg_replace('#[^\\pL\d]+#u', '-', $filename);
// trim
$filename = trim($filename, '-');
// transliterate
if (function_exists('iconv')) {
$filename = iconv('utf-8', 'us-ascii//TRANSLIT', $filename);
}
// lowercase
$filename = strtolower($filename);
// remove unwanted characters
$filename = preg_replace('#[^-\w]+#', '', $filename);
if (empty($filename)) {
return 'n-a';
}
return $filename;
}
}
// usage:
//$safe_filename = FileHelper::sanitizeFileName('#my unsaf&/file\name?"');
?>;

View File

@ -0,0 +1,200 @@
<?php
/**
* @author Rasmus Schultz
* @link http://mindplay.dk
* @copyright Copyright &copy; 2010 Rasmus Schultz
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
*/
/**
* This class allows multi-threaded file downloads, and regular file downloads.
*
* You can use this class when you need to control which users are allowed to
* download a file from a protected area of the local filesystem.
*
* Note that downloading files in this way does result in some memory and processor
* overhead - you should not use this class sporadically for all downloads in your
* application, only when a regular download is not possible for some reason (usually
* because the file in question must not be made available to the general public).
*
* Avoid using this class when you need to log access to a public file - you're
* probably better off using a pre-download logging action, which then redirects to
* the actual file download.
*/
class GDownloadHelper {
/**
* Buffer size (memory requirement per download / user / thread)
*/
const BUFFER_SIZE = 32768; # = 1024*32
/**
* Time limit (time allowed to send one buffer)
*/
const TIME_LIMIT = 30;
/**
* Sends a file to the client.
*
* This is a blocking method, which means that the method-call will not return until
* the client has completed the download of the file (or segment), or until the client
* is disconnected and the connection/download is aborted.
*
* Check the return value of this method to see if the download was successful - this
* if useful, for example, if you need to count and limit the number of times a user can
* download a file; you should increase your counters only if the call returns TRUE.
*
* It is important that your action produces <strong>no other output</strong> before
* or after calling this method, as this will corrupt the downloaded binary file.
*
* Output buffers will be cleared and disabled, and any active CWebLogRoute instances
* (which might otherwise output logging information at the end of the request) will
* be detected and disabled.
*
* If your application might produce any other output after your action completes, you
* should suppress this by using the exit statement at the end of your action.
*
* This method throws a CException if the specified path does not point to a valid file.
*
* This method throws a CHttpException (416) if the requested range is invalid.
*
* @param string full path to a file on the local filesystem being sent to the client.
* @param string optional, alternative filename as the client will see it (defaults to the local filename specified in $path)
* @return boolean true if the download succeeded, false if the connection was aborted prematurely.
*/
public static function send($path, $name=null)
{
// turn off output buffering
while (ob_get_level())
ob_end_clean();
// disable any CWebLogRoutes to prevent them from outputting at the end of the request
foreach (Yii::app()->log->routes as $route)
if ($route instanceof CWebLogRoute)
$route->enabled = false;
// obtain headers:
$envs = '';
foreach ($_ENV as $item => $value)
if (substr($item, 0, 5) == 'HTTP_')
$envs .= $item.' => '.$value."\n";
if (function_exists('apache_request_headers')) {
$headers = apache_request_headers();
foreach ($headers as $header => $value) {
$envs .= "apache: $header = $value\n";
}
}
// obtain filename, if needed:
if (is_null($name))
$name = basename($path);
// verify path and connection status:
if (!is_file($path) || !is_readable($path) || connection_status()!=0)
throw new CException('GDownload::send() : unable to access local file "'.$path.'"');
// obtain filesize:
$size = filesize($path);
// configure download range for multi-threaded / resumed downloads:
if (isset($_ENV['HTTP_RANGE']))
{
list($a, $range) = explode("=", $_ENV['HTTP_RANGE']);
}
else if (function_exists('apache_request_headers'))
{
$headers = apache_request_headers();
if (isset($headers['Range'])) {
list($a, $range) = explode("=", $headers['Range']);
} else {
$range = false;
}
}
else
{
$range = false;
}
// produce required headers for partial downloads:
if ($range)
{
header('HTTP/1.1 206 Partial content');
list($begin, $end) = explode("-", $range);
if ($begin == '')
{
$begin = $size-$end;
$end = $size-1;
}
else if ($end == '')
{
$end = $size-1;
}
$header = 'Content-Range: bytes '.$begin.'-'.$end.'/'.($size);
$size = $end-$begin+1;
}
else
{
$header = false;
$begin = 0;
$end = $size-1;
}
// check range:
if (($begin > $size-1) || ($end > $size-1) || ($begin > $end))
throw new CHttpException(416,'Requested range not satisfiable');
// suppress client-side caching:
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Expires: ".gmdate("D, d M Y H:i:s", mktime(date("H")+2, date("i"), date("s"), date("m"), date("d"), date("Y")))." GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
// send a generic content-type:
header("Content-Type: application/octet-stream");
// send content-range header, if present:
if ($header) header($header);
// send content-length header:
header("Content-Length: ".$size);
// send content-disposition, with special handling for IE:
if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== FALSE)
{
header("Content-Disposition: inline; filename=".str_replace(' ', '%20', $name));
}
else
{
header("Content-Disposition: inline; filename=\"$name\"");
}
// set encoding:
header("Content-Transfer-Encoding: binary\n");
// stream out the binary data:
if ($file = fopen($path, 'rb'))
{
fseek($file, $begin);
$sent = 0;
while ($sent < $size)
{
set_time_limit(self::TIME_LIMIT);
$bytes = $end - ftell($file) + 1;
if ($bytes > self::BUFFER_SIZE)
$bytes = self::BUFFER_SIZE;
echo fread($file, $bytes);
$sent += $bytes;
flush();
if (connection_aborted())
break;
}
fclose($file);
}
// check connection status and return:
$status = (connection_status()==0) and !connection_aborted();
return $status;
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* @class m121114_120630_tbl_candidatos_documentos
* @var $this CDbMigration
* @package application.migrations
*/
class m121114_120630_tbl_candidatos_documentos extends CDbMigration {
public function safeUp() {
$this->createTable('tbl_candidatos_documentos', array(
'id' => 'pk',
'id_candidato' => 'integer NOT NULL',
'fecha' => 'datetime',
'titulo' => 'string',
'nombre_fichero' => 'string',
), 'ENGINE=InnoDB CHARSET=utf8');
$this->addForeignKey('fk_candidatos_documentos_1', 'tbl_candidatos_documentos', 'id_candidato', 'tbl_candidatos', 'id', 'CASCADE', 'CASCADE');
}
public function safeDown() {
$this->dropForeignKey('fk_candidatos_documentos_1', 'tbl_candidatos_documentos');
$this->dropTable('tbl_candidatos_documentos');
}
}
?>

View File

@ -38,6 +38,7 @@
* @property CandidatoTitulacion[] $titulaciones
* @property CandidatoAreaFuncional[] $areasFuncionales
* @property CandidatoCapacidadProfesional[] $capacidadesProfesionales
* @property CandidatoDocumento[] $documentos
*
* @package application.models
*
@ -134,7 +135,7 @@ class Candidato extends CActiveRecord {
array('fecha_nacimiento, observaciones', 'safe'),
array('fecha_nacimiento', 'date', 'format' => 'dd/MM/yyyy'),
array('fecha_nacimiento', 'date', 'format' => 'dd/mm/yyyy'),
// The following rule is used by search().
// Please remove those attributes that should not be searched.
@ -167,7 +168,10 @@ class Candidato extends CActiveRecord {
'areasFuncionalesCount' => array(self::STAT, 'CandidatoAreaFuncional', 'id_candidato'),
'capacidadesProfesionales' => array(self::HAS_MANY, 'CandidatoCapacidadProfesional', 'id_candidato'),
'capacidadesProfesionalesCount' => array(self::STAT, 'CandidatoTitulacion', 'id_candidato'),
'capacidadesProfesionalesCount' => array(self::STAT, 'CandidatoCapacidadProfesional', 'id_candidato'),
'documentos' => array(self::HAS_MANY, 'CandidatoDocumento', 'id_candidato'),
'documentosCount' => array(self::STAT, 'CandidatoDocumento', 'id_candidato'),
);
}
@ -316,7 +320,7 @@ class Candidato extends CActiveRecord {
return parent::beforeSave();
}
protected function afterFind() {
$this->fotografia = new FotografiaPerfil();
$this->fotografia->modelo = $this;
@ -330,8 +334,13 @@ class Candidato extends CActiveRecord {
$this->fotografia->modelo = $this;
}
protected function afterValidate() {
parent::afterValidate();
}
protected function afterSave() {
parent::afterSave();
$this->fecha_nacimiento = Yii::app()->dateFormatter->formatDateTime(CDateTimeParser::parse($this->fecha_nacimiento, 'yyyy-MM-dd'), 'medium', null);
if ($this->isNewRecord)
$this->createUploadDir();
}

View File

@ -0,0 +1,218 @@
<?php
/**
* @class CandidatoDocumento
* @brief Modelo de la tabla "tbl_candidatos_documentos".
*
* The followings are the available columns in table 'tbl_candidatos_documentos':
* @property integer $id
* @property integer $id_candidato
* @property datetime $fecha
* @property string $titulo
* @property string $nombre_fichero
*
* @property CUploadedFile $ficheroDocumento
*
* The followings are the available model relations:
* @property Candidatos $candidato
*/
class CandidatoDocumento extends CActiveRecord {
public $ficheroDocumento;
public function getReadableFileSize($retstring = null) {
// adapted from code at http://aidanlister.com/repos/v/function.size_readable.php
$sizes = array('bytes', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
if ($retstring === null) {
$retstring = '%01.2f %s';
}
$lastsizestring = end($sizes);
foreach ($sizes as $sizestring) {
if ($this->size < 1024) {
break;
}
if ($sizestring != $lastsizestring) {
$this->size /= 1024;
}
}
if ($sizestring == $sizes[0]) {
$retstring = '%01d %s';
} // Bytes aren't normally fractional
return sprintf($retstring, $this->size, $sizestring);
}
/**
* Returns the static model of the specified AR class.
* @param string $className active record class name.
* @return CandidatoDocumento the static model class
*/
public static function model($className = __CLASS__) {
return parent::model($className);
}
/**
* @return string the associated database table name
*/
public function tableName() {
return 'tbl_candidatos_documentos';
}
/**
* @return array validation rules for model attributes.
*/
public function rules() {
// NOTE: you should only define rules for those attributes that
// will receive user inputs.
return array(
array('id_candidato, titulo, fecha', 'required'),
array('nombre_fichero', 'required', 'on' => 'insert'),
array('id_candidato', 'numerical', 'integerOnly' => true),
array('titulo, nombre_fichero', 'length', 'max' => 255),
array('titulo, nombre_fichero, fecha', 'safe'),
array('ficheroDocumento', 'file',
//'types' => 'pdf, doc, docx, txt, odt',
'maxSize' => 1024 * 1024 * 5, // 5MB como máximo
'tooLarge' => Yii::t('profind', 'El documento es demasiado pesado.'),
//'wrongType' => Yii::t('profind', 'Sólo se permiten documentos en formato PDF, DOC, DOCX, TXT, ODT.'),
'allowEmpty' => 'false',
),
// The following rule is used by search().
// Please remove those attributes that should not be searched.
array('id, id_candidato, fecha, titulo, nombre_fichero', 'safe', 'on' => 'search'),
);
}
/**
* @return array relational rules.
*/
public function relations() {
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'candidato' => array(self::BELONGS_TO, 'Candidato', 'id_candidato'),
);
}
/**
* @return array customized attribute labels (name=>label)
*/
public function attributeLabels() {
return array(
'id' => 'ID',
'id_candidato' => 'Candidato',
'fecha' => 'Fecha',
'titulo' => 'Título',
'nombre_fichero' => 'Nombre',
);
}
/**
* Retrieves a list of models based on the current search/filter conditions.
* @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
*/
public function search() {
// Warning: Please modify the following code to remove attributes that
// should not be searched.
$criteria = new CDbCriteria;
$criteria->compare('id', $this->id);
$criteria->compare('id_candidato', $this->id_candidato);
$criteria->compare('fecha', $this->fecha, true);
$criteria->compare('titulo', $this->titulo, true);
$criteria->compare('nombre_fichero', $this->nombre_fichero, true);
return new CActiveDataProvider($this, array(
'criteria' => $criteria,
));
}
protected function afterSave() {
$this->guardarFicheroDocumento();
return parent::afterSave();
}
protected function afterDelete() {
$this->eliminarFicheroDocumento();
return parent::afterDelete();
}
protected function afterConstruct() {
// Valores por defecto
$this->fecha = date('Y-m-d H:i:s', time());
return parent::afterConstruct();
}
protected function beforeValidate() {
if ($this->ficheroDocumento) {
$upload = $this->candidato->getUploadPath();
$this->nombre_fichero = $this->generarNombreFicheroDocumento();
Yii::trace('Nombre para el documento: ' . $this->nombre_fichero, 'application.models.CandidatoDocumento');
}
return parent::beforeSave();
}
/**
* Genera un nombre de fichero para guardar el documento. Se comprueba
* que no exista ningún otro fichero con ese mismo nombre.
* @param CandidatoDocumento $model Documento
* @return string
*/
private function generarNombreFicheroDocumento() {
$cid = $this->candidato->id;
$old_filename = FileHelper::sanitizeFileName(pathinfo($this->ficheroDocumento, PATHINFO_FILENAME));
$ext = pathinfo($this->ficheroDocumento, PATHINFO_EXTENSION);
$folder = $this->candidato->getUploadPath();
$contador = 1;
$filename = $old_filename . '.' . $ext;
// existe el directorio?
if (is_dir($folder)) {
// ya existe el fichero?
while (file_exists($folder . $filename)) {
$filename = $old_filename . '_' . $contador . '.' . $ext;
$contador++;
}
}
return $filename;
}
/*
* Guarda un documento subido por el usuario
* return CUploadedFile fichero subido
*/
private function guardarFicheroDocumento() {
Yii::trace('Guardando el documento ' . $this->ficheroDocumento, 'application.models.CandidatoDocumento');
if (!$this->candidato)
throw new CException(Yii::t('profind', 'Candidato no asignado.'));
if (!$this->ficheroDocumento)
throw new CException(Yii::t('profind', 'Fichero de documento no asignado.'));
$upload = $this->candidato->getUploadPath();
$nombre = $upload . $this->nombre_fichero;
return $this->ficheroDocumento->saveAs($nombre);
}
/*
* Elimina el documento del usuario
* return bool
*/
private function eliminarFicheroDocumento() {
Yii::trace('Eliminando el documento ' . $this->nombre_fichero, 'application.models.CandidatoDocumento');
if (!$this->candidato)
throw new CException(Yii::t('profind', 'Candidato no asignado.'));
$upload = $this->candidato->getUploadPath();
$nombre = $upload . $this->nombre_fichero;
return unlink($nombre);
}
}

View File

@ -765,6 +765,12 @@ textarea {
margin: 0;
list-style: none;
}
.errorMessage {
color: #b94a48;
font-weight: bold;
}
input.error, select.error, textarea.error {
color: #b94a48;
border-color: #b94a48;

View File

@ -0,0 +1,107 @@
<?php
$js_datepicker = <<<JS
jQuery(function($) {
jQuery('.datepicker').datepicker(
jQuery.extend({showMonthAfterYear:false},
jQuery.datepicker.regional['es'],
{'showAnim':'fold','dateFormat':'dd/mm/yy'}
)
);
});
JS;
Yii::app()->clientScript->registerScript('js_datepicker', $js_datepicker, CClientScript::POS_END);
?>
<div class="row-fluid formSep">
<div class="span12">
<label class="control-label"><?php echo Yii::t('profind', 'Currículums'); ?></label>
<div class="controls">
<table class="table templateFrame">
<thead class="templateHead">
<tr>
<th style="width: 20%;"><?php echo Yii::t('profind', 'Fecha'); ?></th>
<th style="width: 30%;"><?php echo Yii::t('profind', 'Título'); ?></th>
<th style="width: 40%;"><?php echo Yii::t('profind', 'Fichero'); ?></th>
<th style="width: 10%;"></th>
</tr>
</thead>
<tbody class="templateTarget">
<?php foreach($candidato->documentos as $i=>$documento): ?>
<tr class="templateContent">
<td>
<?php $this->widget('zii.widgets.jui.CJuiDatePicker', array(
'model' => $documento,
'attribute' => "[$i]fecha",
'language' => 'es',
'options' => array('showAnim' => 'fold', 'dateFormat' => 'dd/mm/yy'),
'htmlOptions' => array('class' => 'span12'),
));
?>
<?php echo CHtml::error($documento, 'fecha'); ?>
</td>
<td>
<?php echo CHtml::activeTextField($documento, "[$i]titulo", array('class' => 'span12')); ?>
<?php echo CHtml::error($documento, 'titulo'); ?>
</td>
<td>
<?php if ($documento->getError('nombre_fichero') != '') : ?>
<?php echo CHtml::activeFileField($documento, "[$i]titulo", array('class' => 'span12')); ?>
<?php echo CHtml::error($documento, 'nombre_fichero'); ?>
<?php else : ?>
<?php echo CHtml::encode($documento->nombre_fichero); ?>
<?php endif; ?>
</td>
<td>
<?php echo CHtml::activeHiddenField($documento, "[$i]id", array('class' => 'pk')); ?>
<?php echo CHtml::activeHiddenField($documento, "[$i]id_candidato"); ?>
<?php echo CHtml::hiddenField("CandidatoDocumento[$i][_borrar]", '0', array('class' => 'to_remove')); ?>
<input type="hidden" class="rowIndex" value="<?php echo $i;?>" />
<?php if (!$documento->hasErrors()) : ?>
<a class="btn btn-small download" href="#"><i class="icon-download"></i></a>
<?php endif; ?>
<a class="btn btn-small remove" href="#"><i class="icon-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<td colspan="4">
<a class="btn btn-small add" href="#"><i class="icon-plus"></i> <?php echo Yii::t('profind', 'Añadir un currículum');?></a>
<textarea class="hide template">
<tr class="templateContent">
<td>
<?php $this->widget('zii.widgets.jui.CJuiDatePicker', array(
'name' => 'CandidatoDocumento[{0}][fecha]',
'value' => date('dd/mm/yy', time()),
'language' => 'es',
'options' => array('showAnim' => 'fold', 'dateFormat' => 'dd/mm/yy'),
'htmlOptions' => array('class' => 'span12 datepicker'),
));
?>
</td>
<td>
<?php echo CHtml::textField('CandidatoDocumento[{0}][titulo]', Yii::t('profind', 'Documento'), array('class' => 'span12')); ?>
</td>
<td>
<?php echo CHtml::fileField('CandidatoDocumento[{0}][nombre_fichero]'); ?>
</td>
<td>
<?php echo CHtml::hiddenField('CandidatoDocumento[{0}][id]', '', array('class' => 'pk')); ?>
<?php echo CHtml::hiddenField('CandidatoDocumento[{0}][id_candidato]'); ?>
<?php echo CHtml::hiddenField('CandidatoDocumento[{0}][_borrar]', '0', array('class' => 'to_remove')); ?>
<input type="hidden" class="rowIndex" value="{0}" />
<a class="btn btn-small remove" href="#"><i class="icon-trash"></i></a>
</td>
</tr>
</textarea>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>

View File

@ -180,6 +180,11 @@ $form = $this->beginWidget('CActiveForm', array(
<?php echo $this->renderPartial('__idiomas', array('candidato' => $candidato)); ?>
</fieldset>
<fieldset>
<legend class="control-label" style="text-align: left;"><?php echo Yii::t('profind', 'Currículums'); ?></legend>
<?php echo $this->renderPartial('__documentos', array('candidato' => $candidato)); ?>
</fieldset>
<fieldset>
<legend><?php echo Yii::t('profind', 'Disponibilidad'); ?></legend>