[opw-web] Allow uploading attachments to projects



commit 7a192870c776302a003bfad02417a541bb02a558
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Sun Mar 9 22:22:13 2014 -0400

    Allow uploading attachments to projects

 classes/class_db.php                               |    3 +-
 classes/class_module.php                           |    1 +
 lang/en-gb.php                                     |   10 +
 modules/mod_attachment.php                         |  203 ++++++++++++++++++++
 modules/mod_view_projects.php                      |   34 ++++
 schema.sql                                         |   11 +
 skins/easterngreen/html/tpl_attachment_add.html    |   33 ++++
 skins/easterngreen/html/tpl_header.html            |    2 +-
 skins/easterngreen/html/tpl_view_project.html      |   11 +
 .../html/tpl_view_project_attachment.html          |   14 ++
 10 files changed, 320 insertions(+), 2 deletions(-)
---
diff --git a/classes/class_db.php b/classes/class_db.php
index 7cd334c..3bdce45 100644
--- a/classes/class_db.php
+++ b/classes/class_db.php
@@ -15,7 +15,8 @@ class db
     var $_TYPE_MAP = array(
         's' => PDO::PARAM_STR,
         'i' => PDO::PARAM_INT,
-        'b' => PDO::PARAM_BOOL
+        'b' => PDO::PARAM_BOOL,
+        'l' => PDO::PARAM_LOB
     );
 
     // Constructor
diff --git a/classes/class_module.php b/classes/class_module.php
index 440258b..a160cf0 100644
--- a/classes/class_module.php
+++ b/classes/class_module.php
@@ -31,6 +31,7 @@ class module
             array('name' => 'manage_programs',   'access' => 'a'),
             array('name' => 'manage_organizations', 'access' => 'a'),
             array('name' => 'notifications',     'access' => 'a'),
+            array('name' => 'attachment',        'access' => 'u'),
         );
     }
 
diff --git a/lang/en-gb.php b/lang/en-gb.php
index c7a3551..1f0628b 100644
--- a/lang/en-gb.php
+++ b/lang/en-gb.php
@@ -282,5 +282,15 @@ $lang_data = array(
     'processing_program'    => 'Processing program #',
     'program_no_mail'       => 'No mails were sent for this program',
 
+    /* Module: attachment */
+    'add_attachment'            => "Add attachment",
+    'attachment'                => "Attachment",
+    'attachment_description'    => "Description",
+    'upload_description_needed' => 'Please supply a description for the attachment',
+    'upload_no_file'            => 'Please select a file to upload',
+    'upload_failed'             => 'Attachment upload failed',
+    'upload_too_large'          => 'Attachment too large (maximum size is 1MB)',
+    'upload_unknown_type'       => 'Attachment type is unsupported (supported: PDF, ODT, TXT)',
+    'confirm_delete_attachment' => 'Are you sure that you want to delete the attachment?'
 );
 
diff --git a/modules/mod_attachment.php b/modules/mod_attachment.php
new file mode 100644
index 0000000..2e3d350
--- /dev/null
+++ b/modules/mod_attachment.php
@@ -0,0 +1,203 @@
+<?php
+/**
+* Pandora v1
+* @license GPLv3 - http://www.opensource.org/licenses/GPL-3.0
+* @copyright (c) 2012 KDE. All rights reserved.
+*/
+
+if (!defined('IN_PANDORA')) exit;
+
+$action = $core->variable('a', 'view');
+$program_id = 0 + $core->variable('prg', 0);
+$project_id = 0 + $core->variable('p', '');
+$attachment_id = 0 + $core->variable('i', '');
+$return_url = $core->variable('r', '');
+$description = $core->variable('description', '', false, true);
+
+$attachment_add = isset($_POST['attachment_add']);
+$confirm = isset($_POST['yes']);
+
+// Keeps things simple to require the program and project ID
+$user->restrict($program_id > 0);
+$user->restrict($project_id > 0);
+
+if (empty($return_url))
+    $return_url ="?q=view_projects&prg={$program_id}&p={$project_id}";
+
+function validate_ids($program_id, $project_id, $attachment_id, $require_owner)
+{
+    global $db, $user;
+
+    $sql = "SELECT COUNT(*) as count " .
+           "FROM {$db->prefix}participants prt ";
+
+    if ($attachment_id > 0)
+        $sql .= "LEFT JOIN {$db->prefix}attachments a " .
+                "ON a.project_id = prt.project_id ";
+
+    $sql .= "WHERE prt.project_id = :project_id AND " .
+                  "prt.program_id = :program_id ";
+
+    if ($attachment_id > 0)
+        $sql .= "AND a.id = :attachment_id ";
+
+    if ($require_owner)
+        $sql .= "AND prt.username = :username " .
+                "AND prt.role = 's' ";
+
+    $row = $db->query($sql,
+                      array('program_id' => $program_id,
+                            'project_id' => $project_id,
+                            'attachment_id' => $attachment_id,
+                            'username' => $user->username),
+                      true);
+
+    return $row['count'] > 0;
+}
+
+if ($action == 'add') {
+    $user->restrict($project_id > 0);
+    $user->restrict(validate_ids($program_id, $project_id, 0, !$user->is_admin));
+
+    $error_message = '';
+
+    if ($attachment_add) {
+        if ($error_message === '') {
+            if ($description == '') {
+                $error_message = $lang->get('upload_description_needed');
+            }
+        }
+
+        if ($error_message === '') {
+            if ($_FILES['file']['error'] == 4)
+                $error_message = $lang->get('upload_no_file');
+        }
+
+        if ($error_message === '') {
+            if ($_FILES['file']['error'] != 0)
+                $error_message = $lang->get('upload_failed');
+        }
+
+        $size = $_FILES['file']['size'];
+        if ($error_message === '') {
+            if ($size > 1000000)
+                $error_message = $lang->get('upload_too_large');
+        }
+
+        $name = basename($_FILES['file']['name']);
+
+        if ($error_message === '') {
+            $content_type = '';
+            $matches = null;
+            if (preg_match('/\.(.*?)$/', $name, $matches)) {
+                $extension = strtolower($matches[1]);
+                if ($extension == 'pdf')
+                    $content_type = 'application/pdf';
+                else if ($extension == 'txt')
+                    $content_type = 'text/plain';
+                else if ($extension == 'odt')
+                    $content_type = 'application/vnd.oasis.opendocument.text';
+            }
+
+            if ($content_type === '')
+                $error_message = $lang->get('upload_unknown_type');
+        }
+
+        if ($error_message === '') {
+            $fp = fopen($_FILES['file']['tmp_name'], 'rb');
+
+            $sql = "INSERT INTO {$db->prefix}attachments " .
+                   "(project_id, name, description, content_type, size, data) " .
+                   " VALUES (:project_id, :name, :description, :content_type, :size, :data)";
+
+            $db->query($sql, array('project_id' => $project_id,
+                                   'name' => $name,
+                                   'description' => $description,
+                                   'content_type' => $content_type,
+                                   'size' => $size,
+                                   'l:data' => $fp));
+
+            $core->redirect($return_url);
+        }
+    }
+
+    // Assign skin data
+    $skin->assign(array(
+        'description'           => htmlspecialchars($description),
+        'cancel_url'            => htmlspecialchars($return_url),
+        'error_message'         => htmlspecialchars($error_message),
+        'error_visibility'      => $skin->visibility($error_message !== '')
+    ));
+
+    // Output the module
+    $module_title = $lang->get('add_attachment');
+    $module_data = $skin->output('tpl_attachment_add');
+
+} else if ($action == 'delete') {
+    $user->restrict($attachment_id > 0);
+    $user->restrict(validate_ids($program_id, $project_id, $attachment_id, !$user->is_admin));
+
+    // Deletion was confirmed
+    if ($confirm)
+    {
+        $sql = "DELETE FROM {$db->prefix}attachments " .
+               "WHERE id = ?";
+        $db->query($sql, $attachment_id);
+
+        $core->redirect($return_url);
+    }
+
+    // Assign confirm box data
+    $skin->assign(array(
+        'message_title'     => $lang->get('confirm_deletion'),
+        'message_body'      => $lang->get('confirm_delete_attachment'),
+        'cancel_url'        => htmlspecialchars($return_url)
+    ));
+
+    // Output the module
+    $module_title = $lang->get('confirm_deletion');
+    $module_data = $skin->output('tpl_confirm_box');
+
+} else if ($action == 'view') {
+    $user->restrict($attachment_id > 0);
+
+    $role = null;
+    $organization_id = null;
+    $user->get_role($program_id, $role, $organization_id);
+
+    // Mentors and admins can see all attachments, otherwise require the project owner
+    $user->restrict(validate_ids($program_id, $project_id, $attachment_id,
+                                 !($user->is_admin || $role == 'm')));
+
+    $sql = "SELECT name, content_type, size, data " .
+           "FROM {$db->prefix}attachments where id=?";
+
+    $stmt = $db->dbh->prepare($sql);
+    $stmt->execute(array($attachment_id));
+
+    $stmt->bindColumn(1, $name, PDO::PARAM_STR);
+    $stmt->bindColumn(2, $content_type, PDO::PARAM_STR);
+    $stmt->bindColumn(3, $size, PDO::PARAM_STR);
+    $stmt->bindColumn(4, $lob, PDO::PARAM_LOB);
+    $stmt->fetch(PDO::FETCH_BOUND);
+
+    $quoted_filename = '"' . preg_replace('/"/', '', $name) . '"';
+
+    header("Content-Type: $content_type");
+    header("Content-Disposition: filename=$quoted_filename");
+    header("Content-Length: $size");
+
+    // mysql PDO backend seems to load PDO's as a string, not stream
+    if (is_string($lob))
+        echo $lob;
+    else
+        fpassthru($lob);
+
+    exit;
+
+} else {
+    // Unknown action
+    $core->redirect($core->path());
+}
+
+?>
\ No newline at end of file
diff --git a/modules/mod_view_projects.php b/modules/mod_view_projects.php
index e074b06..b505276 100644
--- a/modules/mod_view_projects.php
+++ b/modules/mod_view_projects.php
@@ -531,6 +531,37 @@ else if ($action == 'view')
     $can_approve = ($project_data['is_accepted'] == -1 || $project_data['is_accepted'] == 0) && 
$user->is_admin;
     $can_reject  = ($project_data['is_accepted'] == -1 || $project_data['is_accepted'] == 1) && 
$user->is_admin;
 
+    // Attachments
+
+    $can_view_attachments = $is_owner || $role == 'm' || $user->is_admin;
+    $can_delete_attachments = $is_owner || $user->is_admin;
+
+    if ($can_view_attachments) {
+        $sql = "SELECT * FROM {$db->prefix}attachments " .
+               "WHERE project_id = ?";
+        $attachment_data = $db->query($sql, $project_id);
+    } else {
+        $attachment_data = array();
+    }
+
+    $attachments_list = '';
+    foreach ($attachment_data as $row) {
+        $attachment_id = $row['id'];
+        $view_url = "?q=attachment&prg={$program_id}&p={$project_id}&i={$attachment_id}";
+        $delete_url = "?q=attachment&a=delete&prg={$program_id}&p={$project_id}&i={$attachment_id}";
+
+        $skin->assign(array(
+            'attachment_id' => htmlspecialchars($row['id']),
+            'name' => htmlspecialchars($row['name']),
+            'view_url' => htmlspecialchars($view_url),
+            'delete_url' => htmlspecialchars($delete_url),
+            'description' => htmlspecialchars($row['description']),
+            'delete_visibility' => $skin->visibility($can_delete_attachments),
+        ));
+
+        $attachments_list .= $skin->output('tpl_view_project_attachment');
+    }
+
     // Assign final skin data
     $skin->assign(array(
         'program_id'                => $program_id,
@@ -544,9 +575,12 @@ else if ($action == 'view')
         'project_complete'          => $complete,
         'project_result'            => $result,
         'return_url'                => $return_url,
+        'attachments_list'          => $attachments_list,
         'success_message'           => isset($success_message) ? $success_message : '',
         'success_visibility'        => $skin->visibility(empty($success_message), true),
         'edit_visibility'           => $skin->visibility($is_owner || $user->is_admin),
+        'attach_visibility'         => $skin->visibility($is_owner),
+        'attachments_visibility'    => $skin->visibility(count($attachment_data) > 0),
         'delete_visibility'         => $skin->visibility($user->is_admin),
         'mentorship_visibility'     => $skin->visibility($can_mentor),
         'actions_visibility'        => $skin->visibility($is_owner || $can_mentor || $user->is_admin),
diff --git a/schema.sql b/schema.sql
index 763865b..3dac88b 100644
--- a/schema.sql
+++ b/schema.sql
@@ -112,3 +112,14 @@ CREATE TABLE `opw_profiles` (
   `is_admin` tinyint(1) DEFAULT 0,
   PRIMARY KEY (`username`)
 ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+CREATE TABLE `opw_attachments` (
+  `id` mediumint(10) unsigned NOT NULL AUTO_INCREMENT,
+  `project_id` mediumint(10) NOT NULL,
+  `name` varchar(255) NOT NULL,
+  `description` varchar(255) NOT NULL,
+  `content_type` varchar(255) NOT NULL,
+  `size` mediumint(10) unsigned NOT NULL,
+  `data` MEDIUMBLOB NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
diff --git a/skins/easterngreen/html/tpl_attachment_add.html b/skins/easterngreen/html/tpl_attachment_add.html
new file mode 100644
index 0000000..154b547
--- /dev/null
+++ b/skins/easterngreen/html/tpl_attachment_add.html
@@ -0,0 +1,33 @@
+<h1>{{add_attachment}}</h1>
+<hr class="hr-head" />
+
+<div class="alert alert-error [[error_visibility]]">
+    <a class="close" data-dismiss="alert">×</a>
+    [[error_message]]
+</div>
+
+<div class="control-group">
+    <label class="control-label">{{attachment}}</label>
+    <div class="controls">
+        <input type="file" name="file" />
+    </div>
+</div>
+
+<div class="control-group">
+    <label class="control-label">{{attachment_description}}</label>
+    <div class="controls">
+        <input type="text" name="description" maxlength="255" class="input-xxlarge" value="[[description]]" 
/>
+    </div>
+</div>
+
+<div class="form-actions">
+    <button type="submit" name="attachment_add" class="btn btn-primary">
+        <i class="icon-ok-sign icon-white"></i>
+        {{save}}
+    </button>
+
+    <a href="[[cancel_url]]" class="btn">
+        <i class="icon-remove icon-black"></i>
+        {{cancel}}
+    </a>
+</div>
diff --git a/skins/easterngreen/html/tpl_header.html b/skins/easterngreen/html/tpl_header.html
index f2c3865..54d6887 100644
--- a/skins/easterngreen/html/tpl_header.html
+++ b/skins/easterngreen/html/tpl_header.html
@@ -181,4 +181,4 @@ ocalization variable
     <div class="container">
         <div class="row">
             <div class="span12">
-                <form class="form-horizontal" method="post">
+                <form class="form-horizontal" method="post" enctype="multipart/form-data">
diff --git a/skins/easterngreen/html/tpl_view_project.html b/skins/easterngreen/html/tpl_view_project.html
index 11445a4..927a8c7 100644
--- a/skins/easterngreen/html/tpl_view_project.html
+++ b/skins/easterngreen/html/tpl_view_project.html
@@ -57,6 +57,12 @@
         <div class="span8">[[project_result]]</div>
     </div>
 
+    <h4 class="[[attachments_visibility]]">{{attachments}}</h4>
+
+    <table class="table">
+      [[attachments_list]]
+    </table>
+
     <div class="form-actions [[actions_visibility]]">
         <a href="?q=view_projects&amp;a=editor&amp;prg=[[program_id]]&amp;p=[[project_id]]&amp;r=view"
            class="btn btn-primary [[edit_visibility]]">
@@ -74,5 +80,10 @@
             <i class="icon-trash icon-white"></i>
             {{delete}}
         </a>
+
+        <a href="?q=attachment&amp;a=add&amp;prg=[[program_id]]&amp;p=[[project_id]]"
+           class="btn [[add_visibility]]">
+            {{add_attachment}}
+        </a>
     </div>
 </div>
diff --git a/skins/easterngreen/html/tpl_view_project_attachment.html 
b/skins/easterngreen/html/tpl_view_project_attachment.html
new file mode 100644
index 0000000..4b220be
--- /dev/null
+++ b/skins/easterngreen/html/tpl_view_project_attachment.html
@@ -0,0 +1,14 @@
+<tr>
+    <td>
+        <a href="[[view_url]]">[[name]]</a>
+    </td>
+    <td>
+        [[description]]
+    </td>
+    <td class="[[delete_visibility]]">
+        <a href="[[delete_url]]" title="{{delete_attachment}}">
+            <i class="icon-remove"></i>
+        </a>
+    </td>
+</tr>
+


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]