We wrote code for Drupal 5/Ubercart 1.x to implement Classified Ads that could be purchased online and are then copied by Customer Service Reps for print publication. The parts that deal with print aspects could, of course, be skipped for an online-only system.
The basic elements:
- The custom module: 'our_classifieds.module'
- Product Nodes
- Taxonomy
- Attributes
- jQuery snippets
- Requires custom_price module (uc_custom_price)
The overall "philosophy" of our Classifieds installation is to get folks to enter more orderly, clearer information into their ads by fielding the "ad text" into many input fields. This is why we don't just provide an "ad text" field and leave it at that. We use 'form_alter' hooks to add fields specific to the ad subcategory (classification), then use jQuery to pop those values into a non-editable "ad text" field which is a product attribute, in a formatted way, which is then validated for length and other criteria. This is the "built" ad. Customers can edit the ad later, which means we need to re-validate it at that stage as well, making sure the customer hasn't changed the price of the item being sold, for instance, which would affect which ad packages are valid for the ad. We don't try to re-field the text upon editing (re-creating the fielded form they used at the beginning) since that would be too unwieldy.
Many of the functions deal with the 'intersection' between ad text and classification and item price that decides how much the ad will cost the customer. It was critical that we force the customer to choose a classification first, then an ad package that is available for that classification.
Another tricky bit was dealing with the repercussions of multiple taxonomies for each product node. Largely, the stumbling block was Drupal's desire to show ALL terms for a node in the breadcrumb, when in a classified ads system you only want to see the one you selected to get to the 'build your ad' page (the product node). This even required a minor hack to Ubercart core. Any hacks are listed at the end of this posting; we placed the information in blocks that show on the right column when admin or web developer role is viewing any admin page. In that way it's documented so we can deal with it if we upgrade.
We'll start with the core of it, our custom module...
our_classifieds.module
Contains the following functions:
function our_classifieds_form_alter($form_id, &$form)
First a couple of setup items (attribute and pricing constants):
$path = drupal_get_path('module', 'our_classifieds');
_our_classifieds_constants(); // load up some static values
$request = referer_uri(); // looks like "http://blah/blah/catalog/122/automobiles" for form input page
$bpath = explode('/',$request); // 122 = automobiles, not the parent of automobilesThen, we start on the first form we're interested in:
uc_product_add_to_cart_form_[number]
- The function figures out the ad classification, so if none is found, exits the function (to leave other product types untouched)
if ( preg_match("/uc\_product\_add\_to\_cart\_form\_[0-9]+/",$form_id)) {
$subcat = array_pop($bpath);
$subcatid = array_pop($bpath);
$nid = arg(1);
if (is_numeric($nid)) {
$node=node_load($nid);
$package = $node->model;
}
else {
$package = arg(1);
}
if(strpos($package,'ad_package_') ===false) {
$_SESSION['breadcrumb'] = '';
$_SESSION['subcategory'] = '';
$_SESSION['subcatid'] = '';
return;
}
... - Sets product sku (a hidden field)
$form['sku'] = array('#type' => 'hidden',
'#title' => t('SKU'),
'#default_value' => $package,
'#value' => $package,
'#size' => 25,
'#weight' => -10,
); - Makes sure the classification ($subcat) is legal by checking an array hard-coded in function _our_classifieds_get_layout(); places it in a session variable -- If we ever had time to make this a packaged module, this could be read from the taxonomy table for this vocabulary.
- Uses $subcat/session variable to do things with the page's breadcrumb trail. Drupal wants to show all terms. We just want the one the user followed to get here (it's a VERY long list, otherwise).
// following the "return" in our first bullet point...
else {
if($subcatid > 0) {
$_SESSION['subcatid'] = $subcatid;
}
else {
$subcatid = $_SESSION['subcatid'];
}
}
$termobj=taxonomy_get_term($subcatid);
$termobj ? $nicesubcat = $termobj->name : $nicesubcat = $subcat;
// quick and dirty way to be sure subcategory is real:
$checksubcat = _our_classifieds_getLayout($nicesubcat);
if(strpos($checksubcat, "Invalid") !== FALSE) { // it is INVALID
if (isset($_SESSION['subcategory']) && $_SESSION['subcategory'] != '') { // fix with session var
$subcategory = $_SESSION['subcategory'];
}
$_SESSION['layout'] = $checksubcat;
}
else {
$_SESSION['subcategory'] = $nicesubcat;
}
// set breadcrumbs by taxonomy
$parents = array_reverse(taxonomy_get_parents_all($subcatid)); // an object
foreach($parents as $parent) {
$links[] = l($parent->name, "catalog/".$parent->tid."/".$parent->name);
}
drupal_set_breadcrumb($links); - Loads the necessary css and js files:
drupal_add_css("$path/our_classifieds.css");
drupal_add_css("$path/datePicker.css");
drupal_add_js("$path/date.js",'module','header');
drupal_add_js("$path/jquery.datePicker-2.1.2.js",'module','header');
drupal_add_js("$path/makePicker.js",'module','header',FALSE,FALSE);
drupal_add_js("$path/our_classifieds.js,'module','header'); // more about this belowDate plugin source: http://jqueryjs.googlecode.com/svn/trunk/plugins/methods/date.js
DatePicker source: http://www.kelvinluck.com/assets/jquery/datePicker/v2/demo/
We use these Date scripts because the newspaper edition requires lead time between ad submittal and publication. The customer gets a calendar "widget" that only allows valid days to be chosen for when an ad starts. - Changes the type and other attributes of some of the product attribute fields:
$form['attributes'][ADTXT]['#type']='textarea';
$form['attributes'][ADTXT]['#description']='This is the text of your ad -- you will be able to edit it later';
$form['attributes'][ADTXT]['#rows']=6;
$form['attributes'][ADTXT]['#cols']=15;
$form['attributes'][ADTXT]['#validate'] = array('_our_classifieds_validate_adtxt' => array($package,false));
// certain packages only
$form['attributes'][NDAYS]['#size']='2';
$form['attributes'][NDAYS]['#weight']=-10;
$form['attributes'][NDAYS]['#validate'] = array('_our_classifieds_validate_addays' => _our_classifieds_valDays($pkgset));
// ad start date
$form['attributes'][SDATE]['#attributes'] = array('class'=>'date-pick');
$form['attributes'][SDATE]['#size']='10';
$form['submit']['#weight'] = 10;
// subcategory
$form['attributes'][SUBCT]['#value'] = $nicesubcat;
$form['attributes'][SUBCT]['#type'] = 'hidden';
$form['attributes'][SUBCT]['#size'] = '15'; - Figures out what field layout to use based on classification; loads the .js for that layout; adds the custom fields that our jQuery will pour into the adtext attribute field -- This would be very difficult to set up on an admin screen for this to be a contributed module because it is heavily customized. However, other users wishing to make a classified ads system would be fairly likely to want much of what exists here...
$layout = _our_classifieds_getLayout($nicesubcat);
switch($layout) {
case 'Invalid':
$form['submit']= '';
$form['subcat_error'] = array(
'#value'=>'<p class="subcat-error"><strong>ERROR: Missing classification - </strong>This ad package cannot be ordered without first selecting a category and classification</p>',
);
$form['attributes'][NDAYS]['#type']='hidden';
$form['attributes'][SUBCT]['#type']='hidden';
$form['attributes'][SDATE]['#type']='hidden';
$form['attributes'][ADTXT]['#type']='hidden';
break;
case 'Layout1':
drupal_add_js("$path/our_classifieds.js",'module','header');
drupal_add_js("$path/our_c_transport.js",'module','header',FALSE,FALSE);
_our_classifieds_transportation_form($form);
break;
...(etc: 5 possible layouts)
The next form we're interested in:
uc_orderupdate_update_cart_form
We need to change the display settings of some attribute fields:
elseif ($form_id == 'uc_orderupdate_update_cart_form') {
drupal_add_css("$path/our_classifieds.css");
// add the datepick class to the date fields
foreach($form as $key=>$value) {
if(strstr($key,'item-')) {
$package = $form[$key]['model']['#value'];
$form[$key]['attributes'][SDATE]['#attributes'] = array('class'=>'date-pick');
$form[$key]['attributes'][SUBCT]['#type']='hidden';
$form[$key]['attributes'][NDAYS]['#size']=3;
$form[$key]['attributes'][NDAYS]['#validate'] = array('_our_classifieds_validate_addays' => array($package));
$form[$key]['attributes'][ADTXT]['#type']='textarea';
$form[$key]['attributes'][ADTXT]['#attributes'] = array('class'=>'ad-display');
$form[$key]['attributes'][ADTXT]['#cols']=15;
$form[$key]['attributes'][ADTXT]['#rows']=8;
$form[$key]['attributes'][ADTXT]['#resizable'] = false;
$form[$key]['attributes'][ADTXT]['#validate'] = array('_our_classifieds_validate_adtxt' => array($package,true));
$form[$key]['showcat']=array(
'#value'=> "<div class=\"ad-show-subcat\"><strong>Ad classification: </strong>".$form[$key]['attributes'][SUBCT]['#default_value']."</div>",
'#weight'=> -9,
);
}
}
$_SESSION['subcategory'] = '';
}function validate_price($formelement, $min = NULL, $max = NULL)
Called by the validation attribute of the 'asking price' field, this checks the customer entered a price that is within the limits to be eligible for a particular ad package:
$thevalue = preg_replace('/[^0-9\.]/','',$formelement['#value']);
if (is_numeric($thevalue)) {
$thevalue = $thevalue + 0;
} else {
form_error($formelement, t('Price entered must be a number.'));
}
if (isset($min) && ($thevalue < $min)) {
form_error($formelement, t("Price entered must be at least ")."\$$min.");
}
if (isset($max) && ($thevalue > $max)) {
form_error($formelement, t("Price entered must be no higher than ")."\$$max.");
}function our_classifieds_cart_pane($items)
- Checks if the cart contains a Classified Ad (this could be an admin setting stored and retrieved from the variables table, to make this a contributed module, with instructions that classified ad products would all be prefixed with this value):
if(strpos($product->model,'ad_package_') !== false)
And if so, - Creates a 'pane' of instructions to precede the cart items:
foreach($items as $product) {
if(strpos($product->model,'ad_package_') !== false) {
$panes[] = array(
'id' => 'ad_edit_instrux',
'title' => t('Classified Ads Edit Msg'),
'enabled' => TRUE,
'weight' => -9,
'body' => '<div id="how-to-edit" style="width: 98%; border: 2px solid #c00; color: #f00; padding: 0.35em; margin:0; font-weight: bold; font-size: 120%; text-align: center">To edit the text of a Classified Ad, click in the product description below.</div>',
);
}... - Also creates a 'pane' with Cancellation and Publication policies text to follow the cart items (same as above, different weight)
function _our_classifieds_getLayout($subcatname)
Simply an array of classifications (subcategories) with keys indicating which layout to use:
$layouts = array(
"Layout1" => array(
"Antique / Classic / Custom",
"Automobiles",
... ),
"Layout2" => array(
"Airplanes",
"All-Terrain Vehicles", ... ),Checks if the $subcatname is in the $value for each $key. If so, returns the $key
-- Should probably be a hierarchical taxonomy setup instead, if this were written as a "contributed module".
function _our_classifieds_getAdLines($subcat=0,$adtext=0)
Getting values from the main 'our_classifieds_price_calc' function...
- Sets a $charperline (characters per line: this system is set up for a printed newspaper) variable (it changes for some ad types)
- calculates the number of lines in the ad text
function _our_classifieds_ByDaysCalc($lines,$days=null,$package="ad_package_C")
Getting values from the main 'our_classifieds_price_calc' function...
- Deals with the matrix of pricing by lines and days that is used for some ad packages:
$findit = array(
//has to be backward order. We had ranges of days: 1-2, 3,4,5,6-7...
//$lines => ($days=>$ppl, $days=>$ppl, $days=>ppl (price per line))
...
5 => array(3 => 0.00, 2 => 0.00, 1 => 0.00),
4 => array(3 => 0.00, 2 => 0.00, 1 => 0.00),
3 => array(3 => 0.00, 2 => 0.00, 1 => 0.00),
1 => array(3 => 0.00, 2 => 0.00, 1 => 0.00),
);
foreach ($findit as $key=>$value) {
if ($lines >= $key) {
$getit = $value;
break; //Stops as soon as the $lines is found
}
} - Returns the cost of the ad
foreach ($getit as $key=>$value) {
if ($days >= $key) {
$per = $value;
break;
}
}
if ($per > 0) {
return $lines * $days * $per;
}
else return "error: line rate not found";
function our_classifieds_price_calc($item)
This is called by custom_price on the 'package' nodes. That is, uc_custom_price module adds a field for price calculation. See Product Nodes below
- Finds the subcategory/classification, days, package and adtext from the item's attributes:
$subcat = $item->data['attributes'][SUBCT];//['#value']
$days = $item->data['attributes'][NDAYS];
$adtext = $item->data['attributes'][ADTXT];
$package = $item->model;
$pkg_settings = _our_classifieds_getAdRate($package);
$adlines = _our_classifieds_calcNumLines($package,$adtext); - Using the ByDaysCalc, getAdLines, and getAdRate functions, figures out the cost of the ad:
case "ad_package_A":
case "ad_package_C":
$quicktotal = _our_classifieds_ByDaysCalc($adlines,$days,$package);
$extracharge = 0;
$linesover = 0;
break;
default:
$days = $pkg_settings['days'];
$linesover = $adlines - $pkg_settings['lines'];
if ($linesover > 0 && $pkg_settings['extra'] != 'error'){
$extracharge = round(($pkg_settings['extra']*$linesover),2);
$quicktotal = $pkg_settings['cost'] + $extracharge;
}
else {
$extracharge = 0;
$quicktotal = $pkg_settings['cost'];
}
}
function _our_classifieds_getAdRate($package)
Array that is searched to find the right ad cost factors for the package selected:
$rates = array(
'ad_package_A'=>array(
'low'=>0,
'high'=>999999999,
'days'=>1,
'maxdays'=>35,
'lines'=>3,
'maxlines'=>35,
'cost'=>PKGACOST,
'extra'=>PKGAEXTRA,
),
'ad_package_B'=>array(
'low'=>101,
'high'=>500,
'days'=>7,
'maxdays'=>7,
'lines'=>3,
'maxlines'=>35,
'cost'=>PKGBCOST,
'extra'=>PKGBEXTRA,
)...Functions to render custom form fields
- Called by our_classifieds_form_alter. The layout chosen depends on the type of item being advertised: Cars require one set of special fields; Furniture, very different ones.
- Each takes the form reference and adds or changes things in the form, including setting the validation callback for item asking price. Example:
function _our_classifieds_transportation_form(&$form) {
$form['header'] = array(
'#value' => "<h3 class=\"ad-cat\">Please enter your <span>$subcat</span> Transportation ad below</h3>",
'#weight' => -10,
);
# Make: Model: Year:
$form['makemodelyear'] = array(
'#type' => 'fieldset',
'#attributes' => array('class'=>'compact'),
'#title' => t('Make / Model / Year'),
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'#weight' => -9,
);
$form['makemodelyear']['make'] = array(
'#type' => 'textfield',
'#title' => t('Make'),
'#required' => TRUE,
'#size' => '15',
);
... (lots more fields)
# now pull in some standard items (used in most layouts)
_our_classifieds_form_standardfields(&$form); // see below
return $form;
}
_our_classifieds_form_standardfields(&$form)
Just a bit of the standardfields function to show how validations can be set up:
switch ($form['sku']['#value']) {
case 'ad_package_B' : // $101-500
$validate = array(101, 500); break;
case 'ad_package_C' : // $501-2999
$validate = array(501, 2999); break;
...
}
$form['standardfields'] = array(
'#type' => 'fieldset',
'#attributes' => array('class'=>'compact'),
'#title' => '',
'#collapsible' => TRUE,
'#collapsed' => FALSE,
'#weight' => -3,
);
$form['standardfields']['pricing']['asking_price'] = array(
'#type' => 'textfield',
...
'#validate' => array('validate_price' => $validate),
// 'validate_price' is the function to call
);
...
$form['standardfields']['email_for_ad'] = array(
'#type' => 'textfield',
'#title' => t('E-mail address to appear in ad (optional)'),
'#required' => FALSE,
'#size' => '30',
'#weight' => 4,
);
return $form;* See the forms API for further details: http://api.drupal.org/api/file/developer/topics/forms_api_reference.html/5
function _our_classifieds_constants()
This one appears at top of module for easy updating
Sets up ad rating (pricing) values and takes care of some attribute positional discrepancies between development install and live install:
DEFINE ("PKGBCOST",0.00);
DEFINE ("PKGBEXTRA",0.00);
...
DEFINE ("DOMAIN", $_SERVER['SERVER_NAME']);
if (DOMAIN=='xyz') {
// SPECIFIES POSITION IN ATTRIBUTES TABLE: for us, was different on LIVE server than DEV
// adtxt = ad text
// ndays = number of days (certain packages only)
// subct = subcategory
// sdate = ad start date
DEFINE ('ADTXT', 4);
DEFINE ('NDAYS', 6);
DEFINE ('SUBCT', 7);
DEFINE ('SDATE', 5);
}
else {
DEFINE ('ADTXT', 3);
DEFINE ('NDAYS', 4);
DEFINE ('SUBCT', 5);
DEFINE ('SDATE', 6);
}Product Nodes
Our system has just 9 product nodes. They are named in various ways: some ads are free for x lines/x days; some are of a specific type (Real Estate, for instance); and some are named to indicate the price-range of the item being sold and how many lines/days for the base price.
Each has a simple body such as:
For a special package:
* x lines, x consecutive days, $x.xx for each additional line
* Where the ad appears (web, print, special pages)...OR...
For a "custom" ad:
<em>The cost depends on</em>
* length of ad
* number of days the ad runs (must be consecutive)
* Where the ad appearsUbercart adds SKU (fielded as 'model') and pricing fields. We don't use the pricing for Ads (because the length of text a customer enters, and how many days, etc., decides the price), but we enter the last one, 'sell price' setting it to a base value, because it's required.
The uc_custom_price module gives us another field for the Product Nodes: Custom Code. In that field we use:
if(function_exists('our_classifieds_price_calc')) {
$item->price = our_classifieds_price_calc($item);
}
else $item->price = $item->price;(If you don't check for the function's existence first, you get WSOD (white screen o'death) upon turning off our_classifieds module for any reason.)
This of course calls our price calculation function. Since we're not going to make any changes to the 'item' array, we just pass it by value. It's possible that this may change to create an Ubercart Line Item so that we might indicate how the customer's ad was rated for the Cart page.
The only other things in the Product Node are a Product Image (generic-looking "great ad package!" icons) and Taxonomy. Note that in selecting taxonomy terms for a 'product,' we do NOT select the parent of the terms, since we don't want products showing up for purchase before the customer has drilled all the way down to the Classification level. E.g.: The Node is categorized as
# Antiques & Collectibles
# Appliances
# Baby Items
# Bicycles
... but NOT under their parent 'Articles for Sale.'
See below, for more about Taxonomy.
Taxonomy
- Ubercart creates a "Catalog" vocabulary term upon installation. All ad categories are normal taxonomy in the `terms` table with this as the parent vocabulary.
- The store sells both Classified Ads and other products, so 'Place a Classified Ad' is a top level term (no parents) and so are other product groupings.
- Main categories (Transportation, Articles for Sale etc.) have 'Place a Classified' as their parent.
- Subcategories (aka 'classifications') then have the main categories as their parent (no multiple hierarchy).
- Finally, our nine Product Nodes (described above) have multiple taxonomies to show which categories they appear under; some ad packages (xx lines/xx days) are available for lots of different subcategories, and for some subcategories, several packages (product nodes) are available: furniture could be advertised with the xx lines/xx days package, or a custom package, or...
Attributes
We use them to store the ad text, the date to begin running the ad, the number of days to run (for some Products, not others), and the classification (subcategory).
IMPORTANT:
It's been said many times at the Ubercart site, but Create your Attributes before you create your Product Nodes.
- Attributes can be created that when selected alter the pricing of a product. We're not using that part, but we could: to add an extra fee for a border, or a big star, perhaps a photo. (This would require doing something about file uploading, storing, and referencing for the ad.)
- Attributes can have a default value, and whether Required or not, and the Forms API takes care of the 'Required' validation part for you, along with showing it is so on the page.
Handy 'shortcut' for adding attributes
To add attributes to several products: Start on your Store Admin->View Products page. Go to a Product, then paste /edit/attributes/add to the end of the URL to get straight to the important screen (saves 3 clicks and 3 page loads). The /edit/attributes alone shows which ones have been assigned to that product, and allows you to delete or reorder them. Then, use your browser's history (or back-button dropdown) to go straight back to the View Products page. Lather, rinse, repeat. Doesn't help much if you have 40 or 50 products all needing new attributes, though.
jQuery Snippets
Some functions (such as validating a phone number) were common to all, so put in a 'main' .js file. Others were specific to getting the fielded-form values into the ad text (attribute) for a particular classification. For those we built one function ('updateValue'), but different versions lived in different files, the appropriate file being added via drupal_add_js for the subcategory in effect. The "edit-whatever" fields that exist when a customer is entering their ad get the 'whatever' values from our form_alter. For example, in a file called classifieds_transport.js (the below is the full file):
function updateValue() {
var field_values = [];
// this puts all three of these fields together and makes them uppercase.
$('#edit-make, #edit-model, #edit-year').each(function(e) {
if($(this).val()) field_values.push($(this).val().toUpperCase());
});
// these fields are joined by commas
$('#edit-transmission, #edit-cylinder').each(function(e) {
if($(this).val()) field_values.push($(this).val()+',');
});
if($('#edit-mileage').val()) field_values.push($('#edit-mileage').val()+" miles,");
if($('#edit-color').val()) field_values.push($('#edit-color').val()+',');
$('#edit-loaded, #edit-antilock-brakes, #edit-power-steering, #edit-air-conditioning, #edit-cd, #edit-mp3, #edit-am-fm, #edit-stereo, #edit-gps, #edit-dvd-player, #edit-rebuilt-engine, #edit-low-miles, #edit-new-tires, #edit-runs-well, #edit-leather-interior, #edit-sunroof, #edit-moon-roof, #edit-very-clean, #edit-warranty').each(function(e) {
if ($(this).is(':checked')) field_values.push($(this).val()+',');
});
// this one is a 'free-flowing' field for descriptions -- NOT the product attribute called "AdText" that will be saved.
if($('#edit-adtext').val()) field_values.push($('#edit-adtext').val());
if($('#edit-asking-price').val()) field_values.push(formatPrice($('#edit-asking-price').val()));
if($('#edit-or-best-offer') && $('#edit-or-best-offer').is(':checked')) field_values.push($('#edit-or-best-offer').val());
if($('#edit-phone-for-ad').val()) field_values.push(formatPhone($('#edit-phone-for-ad').val()));
if($('#edit-phone-extension').val()) field_values.push("ext. "+$('#edit-phone-extension').val());
if($('#edit-email-for-ad').val()) field_values.push($('#edit-email-for-ad').val());
// fAdText is a variable set up in the shared js file that depends on
// whether we are on development or live server. Only necessary if there were
// attribute position discrepencies between two installations. It's value is
// something like "#edit-attributes-4"
// Note the default join is a space character. This is why fields that should be
// joined by commas were dealt with above by pushing some bunches into a smaller subset.
$(fAdText).val(field_values.join(' '));
}And of course that function is triggered with functions in the main, shared jQuery file: our_classifieds.js
$(document).ready(function () {
setHandlers();
updateValue();
// make sure the customer doesn't type directly in the attribute field that holds the ad text
$(fAdText).focus(function() {
$('.node-add-to-cart:submit').focus();
});
$("#cart-form-products .cart-options").css( 'background-color','#ff9999');
});
function setHandlers(e) {
$(".add_to_cart :checkbox").click(updateValue);
$(".add_to_cart select").change(updateValue);
$(".add_to_cart input:not(textarea"+fAdText+")").blur(updateValue);
}(I've left out a couple of validators for phone numbers and such)
As a small matter... a bit of custom css to deal with the field layouts... especially checkboxes in fieldsets, to keep the user input form to a manageable size and easy to navigate:
/* css for Our Classifieds forms */
.model, .display_price, .sell_price { display: none;}
form .description {clear: both;}
textarea {width: 350px;}
textarea#edit-attributes-3,textarea#edit-attributes-4,textarea.ad-display, .order .ad-text { font-family: Courier, 'Courier New', monospace; width: 16em;}
h3.ad-cat { font-style: italic; text-decoration: underline;}
h3.ad-cat span { color: #c00;}
fieldset.compact .form-item, fieldset.subset .form-item {
display: inline; float: left;
padding: 0 2em 4px 0;
margin: 0;
line-height: 120%;
}
fieldset.compact .form-item label, fieldset.subset .form-item label { display: inline; white-space: nowrap; }
fieldset.subset {background: transparent none; margin: 0; border: none; padding: 0;}
textarea.ad-text { border: 1px solid #fff; background:white; font-size: 12pt; width: 15em; height: 6em; line-height: 120%;}
#how-to-edit { display: block; width: 100%; border: 1px solid #ccc; color: #fff; padding: .25em; margin:0; font-weight: bold;}A note about some small hacks we found necessary
Since hacks are a bad idea, we also needed a way to manage them. We created blocks with the following information and set them to appear only on admin pages in the right column (and only for the roles that would need to see them: webmaster, say).
- Something (possibly CCK's optionwidgets_widget function) seems to cause checkboxes in forms to not use the correct return_value from a hook_form_alter. The solution is to override this in your theme's template.php file (Garland in this case). If an upgrade wipes this out, the code is the same as in /includes/form.inc ~line 1300. It's called phptemplate_checkbox. The key is setting 'value=' $element['#return_value']. One hopes this is no longer necessary in Drupal 6.
- About Ubercart ORDER UPDATE: Had to hack uc_orderupdate.module to show original price and to include hidden 'model (sku)' field for validations.
$form['item-'. $key]['price'] = array('#type' => 'markup', '#value' => "<div class='item-price'><strong>Cost before edits:</strong> $".$item->price."</div>", '#weight' => -8);
$form['item-'. $key]['model'] = array('#type' => 'hidden', '#value' => $item->model, '#weight' => -9);It comes in uc_orderupdate_update_cart_form right after 'qty' form field is set.
- Minor change in UC_CATALOG: In uc_catalog_nodeapi function, added ''
&& (isset($node->model) && strpos($node->model,'ad_package_')===false)' to the end of the'if $a4 == true'condition. This stops the catalog module from overriding the special breadcrumb set in our_classifieds.module's hook_form function. - UBERCART ORDER PANE: we had to hack modules/ ubercart/ uc_order/ uc_order_order_pane.inc to make a field Reps could copy ad text from, for the print edition. Around line 700 in op_products_view_table, I added a 'drupal_add_css' to make the order pane load our_classifieds.css, then during the attributes foreach loop, I added:
if($attribute=='Classified Ad Text'){
$option_rows[] = t('@attribute to paste: <textarea class="order ad-text">@option</textarea>', array('@attribute' =>$attribute, '@option' => $option));
}So since we've already hacked it there, why not show the number of lines for the ad:
if (is_array($options)) {
foreach ($options as $attribute => $option) {
$option_rows[] = t('@attribute: @option', array('@attribute' => $attribute, '@option' => $option));
// new bit here:
if($attribute=='Classified Ad Text') {
$pkgset = _our_classifieds_getAdRate($product->model);
$numlines = _our_classifieds_calcNumLines($product->model,$option);
$option_rows[] = t('Number of lines: @option', array('@option' => $numlines));
}
// end new bit
}
}
Closing
I don't know how useful all this will be to others interested in classified advertising via Ubercart, but I will be happy to answer questions as best I can. I hope some of this will give others a head start on their own systems.
Unfortunately, I have no idea what sorts of changes would be needed to make this work in Drupal 6/Ubercart2. Don't have the time to think about it. Have done some Drupal 6 installs, but little module work as yet, so not sure what all the differences are.
So here it is in a nutshell:
- Custom module .info file:
name = Our Classifieds
description = "Classified Ad Order Entry customizations for Ubercart"
dependencies = uc_store uc_product uc_cart taxonomy uc_custom_price
core = 5.x
php = 5.1 - Custom module .install file:
function our_classifieds_install() {
db_query("UPDATE {system} SET weight=%d WHERE name='%s'",5,"our_classifieds");
} - Custom module "our_classifieds.module": All the functions outlined above.
- Manipulate Add To Cart form
- Manipulate Update Cart form
- Functions to calculate the price of an ad, based on many factors (size, days, category...)
- Functions to lay out an ad creation form to encourage well-put-together ads
- Functions to validate user-entered data (dates, prices and such)
- jQuery files/functions to shoehorn ad creation form fields into attribute fields. One for each type of user input form you need to create:
- articles.js
- transport.js
- ...
- Hooks used:
- hook_form_alter
- hook_cart_pane
Comments
Great Article. This is exactly what i am looking for.
This is great article. This is what i am looking for. I am looking to build a classified site as well where users are sell their ad.
just curious, is there a demo site?
No demo site, only live site at...
https://store.centralmaine.com/
I am looking to have similar sites
How do you go about customizing yours.. Did you use ubercart module and then customize it? Can you explain step by step procedure to me if possible.....
Thanks
I dont think using ubercart
I dont think using ubercart is actually a good choice...
http://backpagestuff.com
.
Do you have a reason for that? Or is this just comment spam? 3 hour account, link to non Drupal site, vague comment. Smells like spam to me.
Michelle
Simple because there are
Simple because there are modules specifically for that, like http://drupal.org/project/ed_classified
p.s: i can't have a sig now? :)
.
Yes, you can have a signature. But when you have a 3 hour old account and respond to a 2 year old post with a vague 1 line comment and a link, you look like a spammer.
ed_classified is for free ads and the module is basically abandoned anyway.
Michelle
Ubercart paid classified ad
I think I have a pretty easy solution. I am setting up a classified ad site using the ed_classified module where only one role can place a classified ad: classified role. Then set up Ubercart to purchase that role. In permissions set it so that only classified role can place an ad, but anonymous role can update own ads. Also make sure that only classified role can reset expiration date. At this point you can tweak it any way you want just by the expiration length of roles and location of buttons.
In order to place an ad the customer needs a link. Control where and when that link shows up and you'll be able to limit the number of ads they buy.
For instance, if you only give them a link to place an ad on the checkout confirmation page, then most users will only be able to place one ad per purchase.
If, on the other hand, you place a link in a block that only shows to classified role, and classified role expires after one week, the customer will be able to place ads for one week.
Not a perfect solution, but pretty close. I've also set up a terms and conditions page that states clearly that anyone spamming the site will have their ads removed and not receive a refund.
Here's my customers site to see (still underconstruction so will change over time) http://abbysbuysellgenealogy.com
:)
Connie Delaney: www.townstate.com