Two CakePHP behaviours to extend MeioUpload

I’ve been using the the Meio Upload Behaviour for a while as its a very handy piece of code, but as is so often the case it doesn’t do quite what I need. In the past I’ve got round this by hacking together a kind of supporting framework in app_controller and scattered about in my models. But the other day I came across the newest version of the code by Juan Basso on Github and decided to dump my mess of spaghetti code and start from scratch.

Aims

The idea is that each model can have its own uploads with their own defaults, for example:

  • Products might need to generate images at 3 different sizes with the smallest zoom cropped.
  • News items might have 2 image sizes but also need to upload a PDF press release.

All of the uploaded files can be viewed and managed centrally from the Upload model.

The changes I wanted to make were as follows:

  1. Use a single Uploads table attached to multiple models (using the Polymorphic behaviour)
  2. The ability to do multiple uploads at once
  3. Rename my uploaded files
  4. Have an UploadVariants model / table with meta information about the thumbnails
  5. Be able to use more of the PHPThumb image options and use them on a per thumbnail basis.

Rather than just take the behaviour and start writing on top of it, I decided to extend the original behaviour (yes you can do this in CakePHP – its just easy to get absorbed in the framework and forget it) and then create an additional behaviour that manages the multiple uploads.

The underlying requirements are quite simple:

(Note 09/07/2009 you don’t actually need the Polymorphic Behaviour, unless you want to do fancy things with your finds)

And my two new behaviours:

At the end of the article is a link to let you download a zipped working CakePHP app with everything in place

The models

First things first. The database tables on which my models are based.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
MySQL Backup
Source Host:           localhost
Source Server Version: 5.0.51b-community-nt
Source Database:       je_meio
Date:                  2009/06/29 15:39:15
*/
 
SET FOREIGN_KEY_CHECKS=0;
#----------------------------
# TABLE STRUCTURE FOR products
#----------------------------
DROP TABLE IF EXISTS products;
CREATE TABLE `products` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `title` VARCHAR(255) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL,
  `created_by` INT(11) DEFAULT NULL,
  `modified_by` INT(11) DEFAULT NULL,
  `created_name` VARCHAR(255) DEFAULT NULL,
  `modified_name` VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
 
#----------------------------
# TABLE STRUCTURE FOR upload_variants
#----------------------------
DROP TABLE IF EXISTS upload_variants;
CREATE TABLE `upload_variants` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `upload_id` INT(11) DEFAULT NULL,
  `variant` VARCHAR(255) DEFAULT NULL,
  `filename` VARCHAR(255) DEFAULT NULL,
  `quick_type` VARCHAR(50) DEFAULT NULL,
  `width` INT(4) DEFAULT NULL,
  `height` INT(4) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL,
  `created_by` INT(11) DEFAULT NULL,
  `modified_by` INT(11) DEFAULT NULL,
  `created_name` VARCHAR(255) DEFAULT NULL,
  `modified_name` VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
 
#----------------------------
# TABLE STRUCTURE FOR uploads
#----------------------------
DROP TABLE IF EXISTS uploads;
CREATE TABLE `uploads` (
  `id` INT(8) UNSIGNED NOT NULL AUTO_INCREMENT,
  `class` VARCHAR(255) DEFAULT 'Upload',
  `foreign_id` INT(11) DEFAULT NULL,
  `alt` VARCHAR(255) DEFAULT NULL,
  `filename` VARCHAR(255) DEFAULT NULL,
  `dir` VARCHAR(255) DEFAULT NULL,
  `mimetype` VARCHAR(255) DEFAULT NULL,
  `quick_type` VARCHAR(50) DEFAULT NULL,
  `filesize` INT(11) UNSIGNED DEFAULT NULL,
  `position` INT(11) DEFAULT '0',
  `height` INT(11) DEFAULT NULL,
  `width` INT(11) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `processed` tinyint(1) DEFAULT '0',
  `modified` datetime DEFAULT NULL,
  `created_by` INT(11) DEFAULT NULL,
  `modified_by` INT(11) DEFAULT NULL,
  `created_name` VARCHAR(255) DEFAULT NULL,
  `modified_name` VARCHAR(255) DEFAULT NULL,
  PRIMARY KEY  (`id`),
  KEY `class` (`class`,`foreign_id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 class Upload extends AppModel {
 
  var $name = 'Upload';
 
  //The Associations below have been created with all possible keys, those that are not needed can be removed
  var $hasMany = array(
    'UploadVariant' => array(
      'className' => 'UploadVariant',
      'foreignKey' => 'upload_id',
      'dependent' => true,
      'conditions' => '',
      'fields' => '',
      'order' => '',
      'limit' => '',
      'offset' => '',
      'exclusive' => '',
      'finderQuery' => '',
      'counterQuery' => ''
    )
  );
 
  var $actsAs = array(
    'Polymorphic',
     'JeMeioUpload' => array(
         'filename' => array(
          'dir' => 'files/uploads',
           'create_directory' => true,
           'max_size' => 2097152,
           'max_dimension' => 'w',
           'thumbnailQuality' => 90,
           'useImageMagick' => false,
           'imageMagickPath' => '/usr/bin/convert',
           'allowed_mime' => array( 'image/gif', 'image/jpeg', 'image/pjpeg', 'image/png'),
           'allowed_ext' => array('.jpg', '.jpeg', '.png', '.gif'),
           'thumbsizes' => array(
             'small'  => array('width' => 90, 'height' => 90),
             'medium' => array('width' => 220, 'height' => 220),
             'large'  => array('width' => 800, 'height' => 600)
           ),
           'default_class' => 'Upload',
           'random_filename' => true
         )
       )
     );
 
}

In the Upload model, just include the Polymorphic behaviour and JeMeioUpload instead of MeioUpload. You could actually use the same defaults as the MeioUpload behaviour itself but there are two additional defaults ‘default_class’ => ‘Upload’, and ‘random_filename’ => true. These both refer to renaming files. As the behaviour can be used to manage uploads from multiple models in a single table it allows you to set the default Upload model. You can also set whether or not you want the files to have the meaningful part replaced with a random string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UploadVariant extends AppModel {
 
  var $name = 'UploadVariant';
 
  //The Associations below have been created with all possible keys, those that are not needed can be removed
  var $belongsTo = array(
    'Upload' => array(
      'className' => 'Upload',
      'foreignKey' => 'upload_id',
      'conditions' => '',
      'fields' => '',
      'order' => ''
    )
  );
 
}

The UploadVariant Model is just as it was baked.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Product extends AppModel {
 
  var $name = 'Product';
 
  var $validate = array(
    'title' => array('notempty')
  );
 
  var $hasMany = array(
        'Upload' => array(
            'className' => 'Upload',    
            'foreignKey' => 'foreign_id',
            'conditions' => array('Upload.class' => 'Product'),
            'dependent' => true,
      'order' => 'Upload.position ASC'
        ),
 
    );
 
    var $actsAs = array(
     'JeMeioUploadPolymorphic'
   );
 
 
 
   var $jeMeioUploadParams = array(
      'filename' => array(
        'dir' => 'files/uploads/',
        'create_directory' => true,
        'allowed_mime' => array('image/jpeg', 'image/pjpeg', 'image/png', 'image/gif'),
        'allowed_ext' => array('.jpg', '.jpeg', '.png', '.gif'),
        'thumbsizes' => array(
          'small'  => array('width'=>60, 'height'=>60, 'image_options' => array('zc' => 1)),
           'large'  => array('width'=>800, 'height'=>400, 'image_options' => array('zc' => 0)),
           'shop'  => array('width'=>265, 'height'=>265, 'image_options' => array('zc' => 0))
        ),
        'random_filename' => false
      )
    );
 
}

Product is a test model set up to demonstrate the multiple uploads and the Polymorphic association to the Upload model. In this case the meaningful part of the filenames will not be replaced with a random string and the ‘small’ thumbnail will be zoom cropped. To find out more about image_options you will need to check the PHPThumb component and the documentation PHPThumb itself (play about and experiment) – what you can and can’t do will also be effected by whether or not you have ImageMagick on your server.

The Controllers

You don’t need to make any changes to your controllers at all.

The Views

There is nothing complicated in the views at all. Just remember to set them up properly for uploads.

The uploads/add.ctp

1
2
3
4
5
6
7
8
9
10
11
<?php echo $form->create('Upload', array('type' => 'file'));?>
	<fieldset>
 		<legend><?php __('Add Upload');?></legend>
	<?php
 
		echo $form->input('alt');
		echo $form->input('filename', array('type' => 'file'));
 
	?>
	</fieldset>
<?php echo $form->end('Submit');?>

The products/add.ctp

This view allows up to 3 uploads to be stored in the associated Upload model.

1
2
3
4
5
6
7
8
9
10
11
<?php echo $form->create('Product', array('type' => 'file'));?>
	<fieldset>
 		<legend><?php __('Add Product');?></legend>
	<?php
		echo $form->input('title');
		echo $form->input('Upload.0.filename', array('type' => 'file'));
		echo $form->input('Upload.1.filename', array('type' => 'file'));
		echo $form->input('Upload.3.filename', array('type' => 'file'));
	?>
	</fieldset>
<?php echo $form->end('Submit');?>

Summary

The one part I’m not terribly happy with at the moment is error messages returning to the views in the Polymorphic associations (e.g. Product example). At the moment if an image or upload can’t be processed then there is no warning or error message. At the moment this is compromise I’m willing to live with.

I could certainly build some kind of component scaffold or something to go in the controllers to return an error message but I think the gain in responsiveness would probably be outweighed by the added complexity. Right now I can’t figure out an elegant way to get error messages from back to the views – if anybody has any suggestions please let me know.
(Note if you are uploading straight into the Upload model then the error messages are unaffected)

There is also the problem of what do you do when in a situation like the following: If you add a new Product successfully but the associated upload has problems? Ideally you would want to redirect to the edit view for that Product but there seems to be no easy way of doing this from a Model / Behaviour.

One solution that I have implemented in the past is to create dedicated views for uploading – so if you do hack your hacks are easy to manage.

I’ve baked a quick and dirty demo that you can download here. Its so basic that it doesn’t even show the uploaded images!

Todo List

  • A helper to generate things like uploads fields complete with delete checkboxes and thumbnail images.
  • Deleting the via the Polymorphic association (don’t think this works right now)

Flirting with Django – part 1

Right now I’ve got a bit of time on my hands and I am slowly catching up with bits and pieces that I have been meaning to do for years. I have also decided to broaden my horizons and learn some Python and Java.

Why Python and Java? Well its simple really; I think Java would be really useful (in this part of the world), both career wise and in terms of being a better programmer; Python? – well my best friend raves about it all the time, its a bit different and Django seems so damn sexy (so far I am loving it).

Over the last couple of years I have dabbled with Django, run through the tutorials etc. but never quite had enough time to get to grips with it and Python at the same time. This time I have a specific project in mind and have decided to stick with it until I have something live and kicking on a web server out there in the big wide world.

(Un)fortunately I just blew my book budget on a pair of Java books – Sams Teach Yourself Java 6 in 21 Days (which is a superb book and I would recommend it to anyone learning Java) and Beginning JSP, JSF & Tomcat Web Development: From Novice to Professional (Beginning from Novice to Professional) (IMHO a bit mixed some of it is great, but chapters on HTML and Javascript are just filler), so in the absence of paper I have had to resort to the (excellent) online versions Django Book and Dive into Python. (Personally I’d take a real book over a screen any day – you can lie in sunshine in a hammock with your laptop and a beer, but you can’t see the screen and if you spill the beer it could be an expensive mistake)

My Setup

I’m developing on my laptop which runs Windows XP Professional. I already have an XAMPP webserver running on my laptop. I also have a copy of the Microsoft Power Toy – Open Command Window here, that lets you open a command prompt in any directory. I’m using Netbeans as an IDE, although I did start off with IDLE (the text editor that comes bundled with Python on Windows) but I got driven mad by all the windows.

The Django documentation runs through everything in detail and is a good place to start http://docs.djangoproject.com/en/dev/intro/install/#intro-install. Here is what I did:

  1. Download + Install Python
  2. Download + Install MySQL Python
  3. Download + Install Django + Set Paths in your windows environment variables
  4. Download + Install PIL (Python Imaging Library) so you can work with images

A few thoughts

Having worked mainly with ASP and PHP, the thing I have found most alien isn’t anything obvious like syntax differences it is the use of libraries . Of course they exist in PHP – think about things like PEAR – but for some reason they are not really a visible part of the language – PEAR is installed almost ubiquitously and in other cases people seem to just grab a library and dump directly into their application somewhere. Built in libraries (think GD or SPL) are just that built in and people don’t actually think about them. I think a lot of this is also a reflection of the nature of most PHP hosting which tends to be in cheap (and often shared) hosting environments where it is difficult to (if not impossible) to use non standard libraries, so people stick to the core.

To me therefore, it feels a little odd to have Django sitting in C:\Python25\Lib\site-packages and my application sitting E:\django\mysite

Getting started – create your project

Go to the directory you want to create your site in and then from the command prompt run:
django-admin.py startproject mysite.
This will create a project called mysite d within your current directory.

Create you app

Within a project you can have any number of applications – I’ve been thinking of an app as basically a plugin – a collection of related code. The idea is that you can take a number of apps and put them together inside a project and viola – a new site. An app might be a products database and shopping cart or it could be a blog or news system. But you can mix them all together and because of the unified Django admin interface they will all fit together perfectly. (The Django book explains it pretty clearly)

What’s the difference between a project and an app? An app is a Web application that does something — e.g., a weblog system, a database of public records or a simple poll app. A project is a collection of configuration and apps for a particular Web site. A project can contain multiple apps. An app can be in multiple projects.

To create an app Go to the directory you want to create your app in and then from the command prompt run:
django-admin.py startapp shop.
(This would create an app called shop within the mysite project)

Create your model

I usually have a pretty accurate database schema down in my notebook before I start any project and although you can import an existing schema and get Django to build the models, the preferred way seems to be to write your models and then get Django to create any necessary database tables based on your models. The latter method is certainly more useful from a learning point of view.

The basic workflow to create your models and synchronise them with the database is very simple indeed:

  1. Edit the models.py file within your app
  2. Generate the SQL for your shop app models with:
    manage.py sql shop.
  3. Create your database tables with:
    manage.py syncdb.

How easy is that?

The one thing to watch out for is that once a table has been created running syncdb will not update the database structure for that table – you either need to do this manually or use find a schema migration tool to use (something I haven’t tried yet)

Templates and Views

Once you’ve got a model or two in place, you probably want to do something with it. So you need to write a view. If you are coming from another MVC system, a Django View is basically equivalent to the Controller. I just followed the Django tutorial and played around from there. I didn’t make any earth shattering discoveries that weren’t in the book.

Django uses templateing engines to produce its output – commonly some HTML (but it could be anything, or even empty for AJAX) mixed in with placeholder tags and filters. When you view the page the place holders are replaced with data from the Model by way of the View. The template system seems to be deliberately sparse, but well thought out. If you need more functionality. you cna extend it by defining your own tags and filters or swap in an alternate template system.

You will need to specify the directory that your templates live in settings.py. e.g.

TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
'E:/django/mysite/templates'
)

If you just want to use Django admin as is you don’t have to do a thing, but if you want to customise it you can override and of the default admin templates just by creating new ones and placing them in a directory called admin inside your templates directory. Again very straight forward.

The one thing that I got stuck on for a short while is that Django doesn’t provide any default templates for your the public facing parts of your application. I don’t think it is that clear on this and I felt a while thinking I was doing something wrong before I realised I actually needed to just make them.

My simple base.html template looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="http://static.mysite.com/css/reset.css" type="text/css" />
<link rel="stylesheet" href="http://static.mysite.com/css/base.css" type="text/css" />
{% block meta %}{% endblock %}
</head>
<body>
<div id="container">
 
	<div id="header">
		{% block header %}{% endblock %}
	</div>
	<div id="content">
		{% block content %}{% endblock %}
		<div class="push"></div>
	</div>
	<div id="footer">
		{% block footer %}{% endblock %}
	</div>
</div>
</body>
</html>

The important thing here is are the placeholder tags e.g. {% block header %}{% endblock %}
In this case this is a placeholder for another template fragment called header so you can extend your templates ad infinitum.

Which brings me on to the only part of my Django experience so far that wasn’t great, it was the hair-pullingly frustrating experience of setting up to serve static files…

Serving Static Files

Django doesn’t like to serve static files (think css, js etc.). Even though I’m just developing on my laptop at the moment, I thought that it would be a good idea to try and mimic a production setup as closely as possible, and reading around the sensible thing to do was just serve all my static files from a sub domain. I’ve also got a copy of XAMPP running on my machine so I set up a subdomain in that to serve static files and experienced a total nightmare. I’m not quite sure why I had so much trouble, but I did, and I would imagine that this is the step where a lot of other people give up. Of course I got it working in the end (or else I probably wouldn’t be writing this, or at least not in a wow Django is so so cool kind of way…)

In the end I did get it working and it is possible that it is slightly weird. My actual Django project directory is within my XAMPP directory structure (because this is where I store all my code), but it is not served by the XAMPP Apache server (yet) – instead it is served using the python server: python manage.py runserver on http://127.0.0.1:8000/

So for the moment my static files are within the Django project (in the static directory) and can be accessed at: http://static.simbiotica.co.uk – in my settings.py has the following settings:


# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = './static/'

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
#MEDIA_URL = 'http://static.simbiotica.co.uk'
MEDIA_URL = '/static/'

Update 19th July 2009

It turns out I was wrong! Thought it was all working, but when I started to dig deeper it wasn’t. Close though I think. For development have gone back to a more conventional setup in my latest post.

Django Files

The story so far…

So far my Django experience has been very positive and I can’t wait to get my first site done. I’ve successfully plugged in the Django Filebrowser into my admin system too. I think the thing I like most is wonderful terseness – you can achive so much writing so little, and from my currently extremely limited Python experience I’m really enjoying the language too.

New business directory seeks businesses

I’ve been working on a business directory website in my spare time for a while and it is now live. The directory is www.blinks.org.uk and is for for companies / organisations / individuals based in the UK who want to promote their businesses. It is totally free to register and you won’t find any registration details sold or passed on to anybody!

At the moment it is still very much in a testing stage and I’m after feedback. Comments suggestions etc. would be greatly appreciated.

The site is written with the wonderful CakePHP framework (and god bless view caching)

blinks.org.uk - Free UK business directory

Tell me more…

The sign up process is quick and easy and only requires that you confirm the email address you register with (this email remains private and is not displayed on the site).

When you have registered you can add as many businesses as you want. You can add the following information for each business you add:

  • Company name
  • Website Address
  • Contact Details
    These are mandatory, although you can elect not to show them on the website
  • Summary
  • Long Description
  • Image or Logo
  • List your business in up to 5 Categories

Click here to register today!