diff --git a/www/protected/controllers/CandidatoController.php b/www/protected/controllers/CandidatoController.php index 1de23e8..f6c9f5c 100644 --- a/www/protected/controllers/CandidatoController.php +++ b/www/protected/controllers/CandidatoController.php @@ -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')); diff --git a/www/protected/helpers/FileHelper.php b/www/protected/helpers/FileHelper.php new file mode 100644 index 0000000..f2b66fc --- /dev/null +++ b/www/protected/helpers/FileHelper.php @@ -0,0 +1,46 @@ +; \ No newline at end of file diff --git a/www/protected/helpers/GDownloadHelper.php b/www/protected/helpers/GDownloadHelper.php new file mode 100644 index 0000000..85fbc11 --- /dev/null +++ b/www/protected/helpers/GDownloadHelper.php @@ -0,0 +1,200 @@ +no other output 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; + } + +} \ No newline at end of file diff --git a/www/protected/migrations/m121114_120630_tbl_candidatos_documentos.php b/www/protected/migrations/m121114_120630_tbl_candidatos_documentos.php new file mode 100644 index 0000000..58ac5fa --- /dev/null +++ b/www/protected/migrations/m121114_120630_tbl_candidatos_documentos.php @@ -0,0 +1,28 @@ +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'); + } +} + +?> \ No newline at end of file diff --git a/www/protected/models/Candidato.php b/www/protected/models/Candidato.php index bf47f08..5e9e575 100644 --- a/www/protected/models/Candidato.php +++ b/www/protected/models/Candidato.php @@ -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(); } diff --git a/www/protected/models/CandidatoDocumento.php b/www/protected/models/CandidatoDocumento.php new file mode 100644 index 0000000..b02eedd --- /dev/null +++ b/www/protected/models/CandidatoDocumento.php @@ -0,0 +1,218 @@ +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); + } +} \ No newline at end of file diff --git a/www/themes/profind/css/profind.css b/www/themes/profind/css/profind.css index 5a663a2..73227c7 100644 --- a/www/themes/profind/css/profind.css +++ b/www/themes/profind/css/profind.css @@ -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; diff --git a/www/themes/profind/views/candidato/__documentos.php b/www/themes/profind/views/candidato/__documentos.php new file mode 100644 index 0000000..675dce6 --- /dev/null +++ b/www/themes/profind/views/candidato/__documentos.php @@ -0,0 +1,107 @@ +clientScript->registerScript('js_datepicker', $js_datepicker, CClientScript::POS_END); +?> + + +
+
+ +
+ + + + + + + + + + + documentos as $i=>$documento): ?> + + + + + + + + + + + + + +
+ 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'), + )); + ?> + + + 'span12')); ?> + + + getError('nombre_fichero') != '') : ?> + 'span12')); ?> + + + nombre_fichero); ?> + + + 'pk')); ?> + + 'to_remove')); ?> + + hasErrors()) : ?> + + + +
+ + +
+
+
+
+ diff --git a/www/themes/profind/views/candidato/_form.php b/www/themes/profind/views/candidato/_form.php index 34b17b6..ccaef37 100644 --- a/www/themes/profind/views/candidato/_form.php +++ b/www/themes/profind/views/candidato/_form.php @@ -180,6 +180,11 @@ $form = $this->beginWidget('CActiveForm', array( renderPartial('__idiomas', array('candidato' => $candidato)); ?> +
+ + renderPartial('__documentos', array('candidato' => $candidato)); ?> +
+