Login

Generate newforms-admin admin.py file

Author:
NL
Posted:
February 16, 2008
Language:
Python
Version:
.96
Score:
16 (after 16 ratings)

This is a utility script that scans a models.py file and automatically outputs the corresponding newforms-admin source code - the code that goes into the admin.py file. The purpose is to simplify the migration to newforms-admin.

Here is what it outputs:

  • an import line that lists only the needed classes from the model.

  • inline editing classes - inheriting from either admin.TabularInline or admin.StackedInline

  • admin options classes containing any original Admin class contents ('fields' is replaced by 'fieldsets' and the value of 'classes' is made into a tuple), plus the following fields whose values are determined automatically: inlines, prepopulated_fields, filter_horizontal, filter_vertical and raw_id_fields

  • invokations of admin.site.register for the generated admin options classes.

Example usage of the script (this will generate the admin.py file for the models.py file in the satchmo.product module):

./new-forms-gen.py satchmo.product > admin.py

  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
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
'''
Utility script for automatically generating a 
newforms-admin admin.py file based on an old-style models.py file.
The generated source code is printed to standard output.

Usage: ./newforms_gen.py my.module > admin.py
where my.module contains a models.py file.
'''

import sys
import os.path
import re
import StringIO
import imp
import copy

def _update_admin_line(s):
    s = re.sub(r"'classes':\s*'(.*?)'", r"'classes': ('\1',)", s)  # make 'classes' a tuple
    s = re.sub(r'fields\s*=', 'fieldsets =', s)                    # change fields into fieldsets
    return s

def generate_newforms_admin(module_name):
    # read the source code
    path = imp.find_module(module_name.split('.')[0])[1]
    filepath = os.path.join(*[path] + module_name.split('.')[1:] + ['models.py'])
    source = open(filepath, 'r').read()

    # initialize some variables
    output = StringIO.StringIO()    
    inline_classes = []
    model_names = []
    model_admin = {}

    # initialize mapping from admin fields to formatting functions
    def format_tuple(data, name, indentation):
        return '%s%s = %s' % (indentation, key, repr(tuple(data)))
    def format_lines(data, name, indentation):
        return '\n'.join([_update_admin_line(line) for line in data if line.strip() != 'pass'])
    def format_dict(data, name, indentation):
        items = ', '.join(("'%s': %s" % (field, value) for field, value in data.items()))
        return '%s%s = {%s}' % (indentation, key, items)
    def format_list_of_names(data, name, indentation):
        return '%s%s = [%s]' % (indentation, name, ', '.join(data))

    default_admin_info ={'admin_lines': [],  
                         'inlines': [],
                         'prepopulated_fields': {},
                         'filter_horizontal': [],
                         'filter_vertical': [],
                         'raw_id_fields': [],
                        }
    field_format_func = {'admin_lines':         format_lines,
                         'inlines':             format_list_of_names,
                         'prepopulated_fields': format_dict,
                         'filter_horizontal':   format_tuple,
                         'filter_vertical':     format_tuple,
                         'raw_id_fields':       format_tuple,}

    parts = re.compile(r'^class (\w+).*$', re.M).split(source)
    for class_name, code in zip(parts[1::2], parts[2::2]):
        # add model to list and initialize admin info for this model
        model_names.append(class_name)
        model_admin[class_name] = copy.deepcopy(default_admin_info)

        # determine indentation 
        first_line = [line for line in code.split('\n') if line.strip()][0]
        indentation = first_line[ : len(first_line) - len(first_line.lstrip())]    

        # find any Admin class declaration
        m = re.search(r'(?m)^%sclass Admin:.*\n((%s[ \t]+.+\n)*)' % (indentation, indentation), code)
        if m:            
            model_admin[class_name]['admin_lines'] = [line.replace(indentation, '', 1) 
                                                      for line in m.group(1).split('\n') if line.strip()]

        # find the fields in this class
        fields = re.findall(r'(?m)^%s(\w+) *= *models\.(\w+).*?\((.*\n((%s[ \t]+.+\n)*))' % (indentation, indentation), code)             

        for field in fields:
            # extract field name, type and rest of line
            field_name, field_type, field_data = field[0:3]

            # extract the field initialization kwargs (only works for simple values, not tuple values)
            params = dict(re.findall(r'(\w+) *= *([a-zA-Z0-9._]+)', field_data))
            first_param = field_data.split(',')[0].strip()

            # find foreign keys with inline editing
            if field_type.endswith('ForeignKey') and 'edit_inline' in params:                        
                if 'TABULAR' in params['edit_inline']:
                    inline_class = 'admin.TabularInline'
                else:
                    inline_class = 'admin.StackedInline'

                fk_class = first_param
                if fk_class.startswith("'") or fk_class.startswith('"'):
                    fk_class = fk_class[1:-1]

                # build inline class
                lines = ['class %s_Inline(%s):' % (class_name, inline_class)]
                lines.append('%smodel = %s' % (indentation, class_name))            
                for input_field, output_field in [('num_in_admin', 'extra'), ]:
                    if input_field in params:
                        lines.append('%s%s = %s' % (indentation, output_field, params[input_field]))

                # add to list of inline classes and add an entry in the ForeignKey model linking to this class
                inline_classes.append('\n'.join(lines) + '\n')
                if not fk_class in model_admin:
                    model_admin[fk_class] = copy.deepcopy(default_admin_info)
                model_admin[fk_class]['inlines'].append('%s_Inline' % class_name)

            # store info used for the admin options class fields
            m = re.search(r'prepopulate_from=(\(.+?\))', field_data)
            if m:            
                model_admin[class_name]['prepopulated_fields'][field_name] = m.group(1)
            if 'filter_interface' in params and ('HORIZONTAL' in params['filter_interface'] or 'True' in params['filter_interface']):
                model_admin[class_name]['filter_horizontal'].append(field_name)
            if 'filter_interface' in params and 'VERTICAL' in params['filter_interface']:
                model_admin[class_name]['filter_vertical'].append(field_name)
            if 'raw_id_admin' in params and params['raw_id_admin'] == 'True':
                model_admin[class_name]['raw_id_fields'].append(field_name)

    register_classes = []    # admin classes to register

    for model in set(model_names) | set(model_admin.keys()):
        if model in model_admin:                    
            # build admin class
            lines = ['class %sOptions(admin.ModelAdmin):' % model]
            for key, value in model_admin[model].items():
                if value:
                    line = field_format_func[key](value, key, indentation)
                    if line:
                        lines.append(line)

            # if the admin class has any contents
            if len(lines) >= 2:                            
                admin_class = '\n'.join(lines)                
                if model in model_names:
                    register_classes.append((model, '%sOptions' % model))
                else:
                    # if the model is not defined in this module comment out the Admin class declaration
                    # (it's the user's responsibility to move relevant parts of the code to the right place)
                    admin_class = '\n'.join(['# ' + line for line in admin_class.split('\n')])                
                   
                output.write(admin_class + '\n\n') # output admin class definition

            # else if the admin class lacks contents but there was an 
            # empty Admin class with just a 'pass' line in the original source
            # and the model class was defined in the current module
            elif model_admin[model]['admin_lines'] and model in model_names:
                register_classes.append((model,))

    for class_names in register_classes:
        output.write('admin.site.register(%s)\n' % ', '.join(class_names))

    # construct final output
    output = output.getvalue()
    lines = []        
    lines.append('from django.contrib import admin')
    lines.append('from django.utils.translation import ugettext_lazy as _\n')
    lines.extend(inline_classes)    
    lines.append(output)
    output = '\n'.join(lines)

    # add an import line that imports all models referenced in the code
    referenced_models = [m for m in model_names if re.search(r'\b%s\b' % m, output)]        
    if referenced_models:
        output = '\n'.join(['from %s.models import ' % module_name + ', '.join(referenced_models), output])

    return output

if __name__ == '__main__':
    print generate_newforms_admin(sys.argv[1])

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 3 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 4 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 8 months ago

Comments

mikko (on June 17, 2008):

Excellent!

#

LuckiDog (on September 11, 2008):

Doesn't work. Do we need some Python Path juju going on?

I get the error:

ImportError: No module named django_site

#

gorango (on October 1, 2008):

s = re.sub(r'fields\s*=', 'fieldsets =', s) silently breaks the search_fields definition.

I fixed my problem manually. My guess is that

s = re.sub(r'(\s)fields\s=', '\1fieldsets =', s) would work, but had no time to test (i am python, django and re newbie, so use on your own...)

#

chadc (on October 27, 2008):

do you have to add something to urls.py or views.py to get this to run?

#

chadc (on October 27, 2008):

the correct usage instructions are

Usage: open terminal to the folder the script is in and run this command

python ./603.py my.module > admin.py

(where my.module is the project.app that contains the models.py file.)

#

hasenj (on March 17, 2009):

Cool, thanks.

I added a small modification to make it work for me, first line under if __name__ == '__main__':

sys.path = [os.path.realpath( os.getcwd() )] + sys.path

This is because I don't have the project directory under pythonpath by default.

#

Please login first before commenting.