Creating a Custom Page in OpenCart - Final
Written by MarketInSG in Tutorials on October 20, 2012 | 24 Comments
This will be the final part of our tutorial. If you have followed our tutorial all the way, you should be able to work out on this final tutorial easily. Continuing from where we left off, in this tutorial, we will complete the example we were working on previously (a simple news extension).
Admin Panel
From where we left off, the page to display news doesn’t have an admin panel. So let us work on it now. The admin panel will just be a simple page where you can view your list of news and add news into the database, allowing it to be displayed on your store front.
Controller File
We will create a file in admin/controller/extension and name it ‘news.php’.
<?php class ControllerExtensionNews extends Controller { private $error = array(); private function install() { $this->db->query("CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "news` ( `news_id` int(11) NOT NULL AUTO_INCREMENT, `date_added` datetime NOT NULL, `status` tinyint(1) NOT NULL, PRIMARY KEY (`news_id`) )"); $this->db->query("CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "news_description` ( `news_description_id` int(11) NOT NULL AUTO_INCREMENT, `news_id` int(11) NOT NULL, `language_id` int(11) NOT NULL, `title` varchar(255) COLLATE utf8_bin NOT NULL, `description` text COLLATE utf8_bin NOT NULL, PRIMARY KEY (`news_description_id`) )"); } public function index() { $this->install(); $this->load->language('extension/news'); $this->load->model('extension/news'); $this->document->setTitle($this->language->get('heading_title')); $this->data['breadcrumbs'] = array(); $this->data['breadcrumbs'][] = array( 'text' => $this->language->get('text_home'), 'href' => $this->url->link('common/home', 'token=' . $this->session->data['token'], 'SSL'), 'separator' => false ); $this->data['breadcrumbs'][] = array( 'text' => $this->language->get('heading_title'), 'href' => $this->url->link('extension/news', 'token=' . $this->session->data['token'], 'SSL'), 'separator' => ' :: ' ); if (isset($this->session->data['success'])) { $this->data['success'] = $this->session->data['success']; unset($this->session->data['success']); } else { $this->data['success'] = ''; } if (isset($this->session->data['warning'])) { $this->data['error'] = $this->session->data['warning']; unset($this->session->data['warning']); } else { $this->data['error'] = ''; } $url = ''; if (isset($this->request->get['page'])) { $page = $this->request->get['page']; $url .= '&page=' . $this->request->get['page']; } else { $page = 1; } $data = array( 'page' => $page, 'limit' => $this->config->get('config_admin_limit'), 'start' => $this->config->get('config_admin_limit') * ($page - 1), ); $total = $this->model_extension_news->countNews(); $pagination = new Pagination(); $pagination->total = $total; $pagination->page = $page; $pagination->limit = $this->config->get('config_admin_limit'); $pagination->text = $this->language->get('text_pagination'); $pagination->url = $this->url->link('extension/news', 'token=' . $this->session->data['token'] . $url . '&page={page}', 'SSL'); $this->data['pagination'] = $pagination->render(); $this->data['heading_title'] = $this->language->get('heading_title'); $this->data['text_title'] = $this->language->get('text_title'); $this->data['text_date'] = $this->language->get('text_date'); $this->data['text_action'] = $this->language->get('text_action'); $this->data['text_edit'] = $this->language->get('text_edit'); $this->data['button_insert'] = $this->language->get('button_insert'); $this->data['button_delete'] = $this->language->get('button_delete'); $this->data['insert'] = $this->url->link('extension/news/insert', '&token=' . $this->session->data['token'], 'SSL'); $this->data['delete'] = $this->url->link('extension/news/delete', 'token=' . $this->session->data['token'], 'SSL'); $this->data['allnews'] = array(); $allnews = $this->model_extension_news->getAllNews($data); foreach ($allnews as $news) { $this->data['allnews'][] = array ( 'news_id' => $news['news_id'], 'title' => $news['title'], 'date_added' => date('d M Y', strtotime($news['date_added'])), 'edit' => $this->url->link('extension/news/edit', '&news_id=' . $news['news_id'] . '&token=' . $this->session->data['token'], 'SSL') ); } $this->template = 'extension/news_list.tpl'; $this->children = array( 'common/header', 'common/footer' ); $this->response->setOutput($this->render()); } public function edit() { $this->load->language('extension/news'); $this->load->model('extension/news'); $this->document->setTitle($this->language->get('heading_title')); if (isset($this->session->data['warning'])) { $this->data['error'] = $this->session->data['warning']; unset($this->session->data['warning']); } else { $this->data['error'] = ''; } if (!isset($this->request->get['news_id'])) { $this->redirect($this->url->link('extension/news', '&token=' . $this->session->data['token'], 'SSL')); } if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) { $this->model_extension_news->editNews($this->request->get['news_id'], $this->request->post); $this->session->data['success'] = $this->language->get('text_success'); $this->redirect($this->url->link('extension/news', 'token=' . $this->session->data['token'], 'SSL')); } $this->data['breadcrumbs'] = array(); $this->data['breadcrumbs'][] = array( 'text' => $this->language->get('text_home'), 'href' => $this->url->link('common/home', 'token=' . $this->session->data['token'], 'SSL'), 'separator' => false ); $this->data['breadcrumbs'][] = array( 'text' => $this->language->get('heading_title'), 'href' => $this->url->link('extension/news', 'token=' . $this->session->data['token'], 'SSL'), 'separator' => ' :: ' ); $this->data['action'] = $this->url->link('extension/news/edit', '&news_id=' . $this->request->get['news_id'] . '&token=' . $this->session->data['token'], 'SSL'); $this->data['cancel'] = $this->url->link('extension/news', '&token=' . $this->session->data['token'], 'SSL'); $this->data['token'] = $this->session->data['token']; $this->form(); } public function insert() { $this->load->language('extension/news'); $this->load->model('extension/news'); $this->document->setTitle($this->language->get('heading_title')); if (isset($this->session->data['warning'])) { $this->data['error'] = $this->session->data['warning']; unset($this->session->data['warning']); } else { $this->data['error'] = ''; } if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) { $this->model_extension_news->addNews($this->request->post); $this->session->data['success'] = $this->language->get('text_success'); $this->redirect($this->url->link('extension/news', 'token=' . $this->session->data['token'], 'SSL')); } $this->data['breadcrumbs'] = array(); $this->data['breadcrumbs'][] = array( 'text' => $this->language->get('text_home'), 'href' => $this->url->link('common/home', 'token=' . $this->session->data['token'], 'SSL'), 'separator' => false ); $this->data['breadcrumbs'][] = array( 'text' => $this->language->get('heading_title'), 'href' => $this->url->link('extension/news', 'token=' . $this->session->data['token'], 'SSL'), 'separator' => ' :: ' ); $this->data['action'] = $this->url->link('extension/news/insert', '&token=' . $this->session->data['token'], 'SSL'); $this->data['cancel'] = $this->url->link('extension/news', '&token=' . $this->session->data['token'], 'SSL'); $this->data['token'] = $this->session->data['token']; $this->form(); } private function form() { $this->load->language('extension/news'); $this->load->model('extension/news'); $this->load->model('localisation/language'); $this->data['heading_title'] = $this->language->get('heading_title'); $this->data['text_title'] = $this->language->get('text_title'); $this->data['text_description'] = $this->language->get('text_description'); $this->data['text_status'] = $this->language->get('text_status'); $this->data['text_keyword'] = $this->language->get('text_keyword'); $this->data['text_enabled'] = $this->language->get('text_enabled'); $this->data['text_disabled'] = $this->language->get('text_disabled'); $this->data['button_submit'] = $this->language->get('button_submit'); $this->data['button_cancel'] = $this->language->get('button_cancel'); $this->data['languages'] = $this->model_localisation_language->getLanguages(); if (isset($this->request->get['news_id'])) { $news = $this->model_extension_news->getNews($this->request->get['news_id']); } else { $news = ''; } if (isset($this->request->post['news'])) { $this->data['news'] = $this->request->post['news']; } elseif (!empty($news)) { $this->data['news'] = $this->model_extension_news->getNewsDescription($this->request->get['news_id']); } else { $this->data['news'] = ''; } if (isset($this->request->post['keyword'])) { $this->data['keyword'] = $this->request->post['keyword']; } elseif (!empty($news)) { $this->data['keyword'] = $news['keyword']; } else { $this->data['keyword'] = ''; } if (isset($this->request->post['status'])) { $this->data['status'] = $this->request->post['status']; } elseif (!empty($news)) { $this->data['status'] = $news['status']; } else { $this->data['status'] = ''; } $this->template = 'extension/news_form.tpl'; $this->children = array( 'common/header', 'common/footer' ); $this->response->setOutput($this->render()); } public function delete() { $this->load->language('extension/news'); $this->load->model('extension/news'); $this->document->setTitle($this->language->get('heading_title')); if (isset($this->request->post['selected']) && $this->validateDelete()) { foreach ($this->request->post['selected'] as $id) { $this->model_extension_news->deleteNews($id); } $this->session->data['success'] = $this->language->get('text_success'); } $this->redirect($this->url->link('extension/news', 'token=' . $this->session->data['token'], 'SSL')); } private function validateDelete() { if (!$this->user->hasPermission('modify', 'extension/news')) { $this->error['warning'] = $this->language->get('error_permission'); $this->session->data['warning'] = $this->language->get('error_permission'); } if (!$this->error) { return true; } else { return false; } } private function validate() { if (!$this->user->hasPermission('modify', 'extension/news')) { $this->error['warning'] = $this->language->get('error_permission'); $this->session->data['warning'] = $this->language->get('error_permission'); } if (!$this->error) { return true; } else { return false; } } } ?>
Seems long and complicating? Nah, it’s quite simple. For the function ‘list’, it list out all your news. When you click on the ‘Insert’ or ‘Edit’ button, it will render the form instead of displaying the list. The ‘install’ function will just be checking if the news table had been added. If it is not in your database, it will add those tables for you. Simple! Not much teaching will be done in this tutorial, it’s time to get your hands dirty. Work on it! You will surely get the structure of the files after reading our previous two tutorials.
Template File
Now, since you have a ‘list’ view and a ‘form’ view, you will need to create 2 template files.
news_form.tpl
<?php echo $header; ?> <div id="content"> <div class="breadcrumb"> <?php foreach ($breadcrumbs as $breadcrumb) { ?> <?php echo $breadcrumb['separator']; ?><a href="<?php echo $breadcrumb['href']; ?>"><?php echo $breadcrumb['text']; ?></a> <?php } ?> </div> <?php if ($error) { ?> <div class="warning"><?php echo $error; ?></div> <?php } ?> <div class="box"> <div class="heading"> <h1><img src="view/image/feed.png" alt="" /> <?php echo $heading_title; ?></h1> <div class="buttons"><a onclick="$('#form').submit();" class="button"><?php echo $button_submit; ?></a><a onclick="" class="button"><?php echo $button_cancel; ?></a></div> </div> <div class="content"> <form action="<?php echo $action; ?>" method="post" enctype="multipart/form-data" id="form"> <div id="language" class="htabs"> <?php foreach ($languages as $language) { ?> <a href="#tab-language-<?php echo $language['language_id']; ?>"><img src="view/image/flags/<?php echo $language['image']; ?>" title="<?php echo $language['name']; ?>" /> <?php echo $language['name']; ?></a> <?php } ?> </div> <?php foreach ($languages as $language) { ?> <div id="tab-language-<?php echo $language['language_id']; ?>"> <table class="form"> <tr> <td class="left"><?php echo $text_title; ?></td> <td><input type="text" name="news[<?php echo $language['language_id']; ?>][title]" value="<?php echo isset($news[$language['language_id']]) ? $news[$language['language_id']]['title'] : ''; ?>" /></td> </tr> <tr> <td><?php echo $text_description; ?></td> <td><textarea name="news[<?php echo $language['language_id']; ?>][description]" id="description-<?php echo $language['language_id']; ?>"><?php echo isset($news[$language['language_id']]) ? $news[$language['language_id']]['description'] : ''; ?></textarea></td> </tr> </table> </div> <?php } ?> <table class="form"> <tr> <td><?php echo $text_keyword; ?></td> <td><input type="text" value="<?php echo $keyword; ?>" name="keyword" /></td> </tr> <tr> <td><?php echo $text_status; ?></td> <td><select name="status"> <option <?php if ($status == '1') { ?>selected="selected" <?php } ?>value="1"><?php echo $text_enabled; ?></option> <option <?php if ($status == '0') { ?>selected="selected" <?php } ?>value="0"><?php echo $text_disabled; ?></option> </select></td> </tr> </table> </form> </div> </div> </div> <script type="text/javascript" src="view/javascript/ckeditor/ckeditor.js"></script> <script type="text/javascript"><!-- <?php foreach ($languages as $language) { ?> CKEDITOR.replace('description-<?php echo $language['language_id']; ?>', { filebrowserBrowseUrl: 'index.php?route=common/filemanager&token=<?php echo $token; ?>', filebrowserImageBrowseUrl: 'index.php?route=common/filemanager&token=<?php echo $token; ?>', filebrowserFlashBrowseUrl: 'index.php?route=common/filemanager&token=<?php echo $token; ?>', filebrowserUploadUrl: 'index.php?route=common/filemanager&token=<?php echo $token; ?>', filebrowserImageUploadUrl: 'index.php?route=common/filemanager&token=<?php echo $token; ?>', filebrowserFlashUploadUrl: 'index.php?route=common/filemanager&token=<?php echo $token; ?>' }); <?php } ?> //--></script> <script type="text/javascript"><!-- $('#language a').tabs(); //--></script> <?php echo $footer; ?>
This is going to be easy. It displays a form with fields for you to fill it up. Making use of the built in text editor for OpenCart, it turns your textarea box into a WYSIWYG editor. Now you have made yourself a form!
news_list.tpl
<?php echo $header; ?> <div id="content"> <div class="breadcrumb"> <?php foreach ($breadcrumbs as $breadcrumb) { ?> <?php echo $breadcrumb['separator']; ?><a href="<?php echo $breadcrumb['href']; ?>"><?php echo $breadcrumb['text']; ?></a> <?php } ?> </div> <?php if ($success) { ?> <div class="success"><?php echo $success; ?></div> <?php } ?> <?php if ($error) { ?> <div class="warning"><?php echo $error; ?></div> <?php } ?> <div class="box"> <div class="heading"> <h1><img src="view/image/feed.png" alt="" /> <?php echo $heading_title; ?></h1> <div class="buttons"><a onclick="location = '<?php echo $insert; ?>'" class="button"><?php echo $button_insert; ?></a><a onclick="$('#form').submit();" class="button"><?php echo $button_delete; ?></a></div> </div> <div class="content"> <form action="<?php echo $delete; ?>" method="post" enctype="multipart/form-data" id="form"> <table class="list"> <thead> <tr> <td width="1" style="text-align: center;"><input type="checkbox" onclick="$('input[name*=\'selected\']').attr('checked', this.checked);" /></td> <td class="left"><?php echo $text_title; ?></td> <td class="left"><?php echo $text_date; ?></td> <td class="right"><?php echo $text_action; ?></td> </tr> </thead> <tbody> <?php if ($allnews) { ?> <?php foreach ($allnews as $news) { ?> <tr> <td width="1" style="text-align: center;"><input type="checkbox" name="selected[]" value="<?php echo $news['news_id']; ?>" /></td> <td class="left"><?php echo $news['title']; ?></td> <td class="left"><?php echo $news['date_added']; ?></td> <td class="right">[ <a href="<?php echo $news['edit']; ?>"><?php echo $text_edit; ?></a> ]</td> </tr> <?php } ?> <?php } ?> </tbody> </table> </form> <div class="pagination"><?php echo $pagination; ?></div> <div style="text-align:center; color:#222222;">Advance News System v1.2 by <a target="_blank" href="http://www.marketinsg.com">MarketInSG</a><br>Donate to <a href="http://www.marketinsg.com/donate" target="_blank">MarketInSG</a></div> </div> </div> </div> <?php echo $footer; ?>
This is also similar to what we had previously. We get the news from the database and list them all out for you. So this template is responsible for the listing of news.
Language File
Of course! How can we miss out this file?! This file, had been called for from our controller, so we should have news.php in admin/language/english/ folder.
<?php // Heading $_['heading_title'] = 'News'; // Text $_['text_title'] = 'Title'; $_['text_description'] = 'Description'; $_['text_date'] = 'Date Added'; $_['text_action'] = 'Action'; $_['text_status'] = 'Status'; $_['text_keyword'] = 'SEO Keyword'; // Success $_['text_success'] = 'You have successfully modified news!'; // Error $_['error_permission'] = 'Warning: You do not have permission to modify news!'; ?>
If we explain this again, we’re gonna explain it for the third time! You can always refer to our previous tutorials for explanation on the language file, the functions, and how do you use it.
Model File
No, we didn’t forget the model file! We will need this. So let’s place news.php in admin/model/extension/ folder. The folder shouldn’t be there, so just create one.
<?php class ModelExtensionNews extends Model { public function addNews($data) { $this->db->query("INSERT INTO " . DB_PREFIX . "news SET date_added = NOW(), status = '" . (int)$data['status'] . "'"); $news_id = $this->db->getLastId(); foreach ($data['news'] as $key => $value) { $this->db->query("INSERT INTO " . DB_PREFIX ."news_description SET news_id = '" . (int)$news_id . "', language_id = '" . (int)$key . "', title = '" . $this->db->escape($value['title']) . "', description = '" . $this->db->escape($value['description']) . "'"); } if ($data['keyword']) { $this->db->query("INSERT INTO " . DB_PREFIX . "url_alias SET query = 'news_id=" . (int)$news_id . "', keyword = '" . $this->db->escape($data['keyword']) . "'"); } } public function editNews($id, $data) { $this->db->query("UPDATE " . DB_PREFIX . "news SET status = '" . (int)$data['status'] . "' WHERE news_id = '" . (int)$id . "'"); $this->db->query("DELETE FROM " . DB_PREFIX . "news_description WHERE news_id = '" . (int)$id. "'"); foreach ($data['news'] as $key => $value) { $this->db->query("INSERT INTO " . DB_PREFIX ."news_description SET news_id = '" . (int)$id . "', language_id = '" . (int)$key . "', title = '" . $this->db->escape($value['title']) . "', description = '" . $this->db->escape($value['description']) . "'"); } $this->db->query("DELETE FROM " . DB_PREFIX . "url_alias WHERE query = 'news_id=" . (int)$id. "'"); if ($data['keyword']) { $this->db->query("INSERT INTO " . DB_PREFIX . "url_alias SET query = 'news_id=" . (int)$id . "', keyword = '" . $this->db->escape($data['keyword']) . "'"); } } public function getNews($id) { $query = $this->db->query("SELECT DISTINCT *, (SELECT keyword FROM " . DB_PREFIX . "url_alias WHERE query = 'news_id=" . (int)$id . "') AS keyword FROM " . DB_PREFIX . "news WHERE news_id = '" . (int)$id . "'"); if ($query->num_rows) { return $query->row; } else { return false; } } public function getNewsDescription($id) { $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "news_description WHERE news_id = '" . (int)$id . "'"); foreach ($query->rows as $result) { $news_description[$result['language_id']] = array( 'title' => $result['title'], 'description' => $result['description'] ); } return $news_description; } public function getAllNews($data) { $sql = "SELECT * FROM " . DB_PREFIX . "news n LEFT JOIN " . DB_PREFIX . "news_description nd ON n.news_id = nd.news_id WHERE nd.language_id = '" . (int)$this->config->get('config_language_id') . "' ORDER BY date_added DESC"; if (isset($data['start']) || isset($data['limit'])) { if ($data['start'] < 0) { $data['start'] = 0; } if ($data['limit'] < 1) { $data['limit'] = 20; } $sql .= " LIMIT " . (int)$data['start'] . "," . (int)$data['limit']; } $query = $this->db->query($sql); return $query->rows; } public function deleteNews($id) { $this->db->query("DELETE FROM " . DB_PREFIX . "news WHERE news_id = '" . (int)$id . "'"); $this->db->query("DELETE FROM " . DB_PREFIX . "news_description WHERE news_id = '" . (int)$id . "'"); $this->db->query("DELETE FROM " . DB_PREFIX . "url_alias WHERE query = 'news_id=" . (int)$id. "'"); } public function countNews() { $count = $this->db->query("SELECT * FROM " . DB_PREFIX . "news"); return $count->num_rows; } } ?>
There we go! Isn’t this simple? Simple functions to insert, get, and update your database.
Putting it together
As usual, I won’t be missing out the downloads for you guys! I have put together a fully functional news extension with SEO URL & multi-language capabilities. Best of all, it’s free! You can get your free copy of this extension at News system. Just download a copy of the extension and you can play around with the codes. vQmod will be needed for this extension as it adds a link to your store’s footer. Alternatively, you can access the news page from your store front at http://yourstore.com/index.php?route=information/news.
As this is the last and final part of our ‘Creating a Custom Page in OpenCart’ tutorial, should you wish to suggest a new topic for us to write on, also you can always comment below. Follow MarketInSG on Facebook to get updates on our new tutorials or follow OpenCartNews to be up to date on OpenCartNews.
24 Comments on "Creating a Custom Page in OpenCart - Final"
Do these tutorials apply to opencart 2.0 as well?
Would be great to see Creating a Custom Page in OpenCart tutorials for opencart 2.0
no there are few changes like in controller and view.
use $data instead $this->data and some changes in view files, just compare them with
default view files.
Hello sir,
Thanks for sharing this tutorial
I have one more issue, its not showing in ‘Extension/News’.
And where i put both templates file:
news_form.tpl
news_list.tpl
Hi Neel,
I had the same problem, I did it to solve:
Is need to give permission for admin user. It’s necessary to change the oc_user_groups table in oc database.
The content is in JSON format, so you need to add the new permission in access node and modify node
access: “extension\/news”
modify: “extension\/news”
The view files you can put into admin/view/template/extension
By the way, I am using opencart v 2.1.0.1
Regards
Please suggest how to create custom checkout page in oc
Absolutely brilliant, MarketInSG, all of your tuts. I’m truly impressed and grateful. Even if I have managed to solve all my OC issues without help I never felt comfortable in the OC environment. Reading these three pieces, though made me understand OC’s structure and functions on a new and higher level. Strange, coz I’ve read many other good tuts but none of them has given me the same confidence to actually start coding. Too bad you’re not the owner of OC! I believe you would succeed in making OC the best cart on the planet, easily. As of May 2014 I’m not all that sure OC has a future. Not a bright one anyway. I think OC will continue undeveloped and that’s really a pity considering its true potential.
Decorating for your theme can be taken up a notch from the hand-lettered, felt and Velcro displays found in elementary school.
You can also use the magazine racks and other racks to hold things like plants, newspapers and wine bottles.
Silhouettes of trees, planets and mountains are all good
examples of interesting, stark backgrounds.
Thanks ….. MarketInSG
This post is really very help full.
Hi! I’ve been reading your blog for a while now and finally got
the courage to go ahead and give you a shout out from Porter Texas!
Just wanted to tell you keep up the excellent job!
Also visit my web site build twitter followers
How are the SEO query strings being applied for each news item here on the front end? (for example mapping news_id=2 to seo-keyword)? I can see they are being saved to the DB, but then they are not being used, or as far as I can see, picked up anywhere? I’ve been struggling to find how additional query string parameters are mapped out anywhere, can you suggest what to do?
Very nice tutorial. I have downloaded the extension.
Could you please explain how the file news.php in the admin/model/settings folder works and what it does?
Thanks
You got numerous constructive points there. I created a search on the issue and identified virtually all peoples will agree with your blog.
Ok, I wasn’t going bonkers, it still didn’t work. As a fix I just hardcoded the correct link in “/admin/view/template/common/header.tpl” as such :-
<a href="session->data['token']; ?>”>
I put the above code just below the ‘$text_feed’ line.
My Bad… forgot to import sql. A thousand appologies 8)
Alas, it appears af it does not work on the latest version 100%. The link for ‘Extension/News’ is missing, as much as the ‘News’ heading does show up on the home page. So much for the quick and easy route.
awesome tutorial, thanks
Hi
I am using this code and its working fine and in the backend under design layout for different pages, it is dispayed as default. I need to create 3-4 different pages using this code 3-4 times with file names changed. Can i change the word default to something else under design->layout ?
Please let me know if its possibe as i need urgent help regarding this.
Thanks & Regards
Nisha
You are simply great!!!
Is it work on 1.9.4.5 opencart version ? Thanks !
Wow, this is great! Thanks for the great tutorial and website.
Is there a way we can suggest tutorials?
you can suggest to me if you would like me to write and I will write if I have time
thanks for giving this tutorial
Thanks for your tremendous series on opencart. Looking forward to your next tutorials. Thanks
Trackbacks for this post