Changeset 406 in main


Ignore:
Timestamp:
10/19/11 13:35:09 (8 years ago)
Author:
pcosquer
Message:

csvimport: refactor and document

Location:
trunk
Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/docs/conf.py

    r347 r406  
    2424global_settings.DOCUMENTS_DIR = "/tmp" 
    2525global_settings.THUMBNAILS_DIR = "/tmp" 
     26global_settings.THUMBNAILS_URL = "/media/thumbnails/" 
    2627settings.configure() 
    2728 
  • trunk/docs/modules.rst

    r335 r406  
    99    base_views 
    1010    controllers 
     11    csvimport 
    1112    exceptions 
    1213    filehandlers 
  • trunk/openPLM/plmapp/csvimport.py

    r405 r406  
     1u""" 
     2Tools to import data from a CSV file. 
     3""" 
     4 
    15import re 
     6from abc import ABCMeta, abstractmethod 
    27from functools import partial 
    38from itertools import islice 
     
    1318 
    1419 
    15  
     20# function that replace spaces by an underscore 
    1621_to_underscore = partial(re.compile(r"\s+").sub, "_") 
    1722 
    1823class CSVImportError(StandardError): 
     24    """ 
     25    Exception raised when an import of a CSV file fails. 
     26 
     27    .. attribute: errors 
     28 
     29        dictionary (line -> :class:`~django.forms.util.ErrorList`) of all 
     30        detected errors. 
     31    """ 
    1932 
    2033    def __init__(self, errors): 
    2134        self.errors = errors 
    2235 
    23     def __str__(self): 
     36    def __unicode__(self): 
    2437        details = self.errors.as_text() 
    25         return "CSVImportError:\n\t" + details  
     38        return u"CSVImportError:\n\t" + details  
    2639 
    2740class Preview(object): 
     41    u""" 
     42    Preview of a CSV file. 
     43 
     44    :param csv_file: the csv file being parsed 
     45    :type csv_file: a file like object 
     46    :param encoding: encoding of the file (`utf-8`, `ascii`, etc.) 
     47    :param known_headers: collection of headers that may be valid 
     48 
     49    .. attribute:: headers 
     50 
     51        headers of the CSV file 
     52    .. attribute:: guessed_headers 
     53 
     54        headers translated according to *known_headers*, an header that can 
     55        not be translated is replaced by `None` 
     56    .. attribute:: rows 
     57 
     58        first non-headers rows of the file (at most two rows) 
     59    """ 
    2860 
    2961    def __init__(self, csv_file, encoding, known_headers): 
    3062        reader = UnicodeReader(csv_file, encoding=encoding) 
    3163        self.headers = reader.next() 
    32         self.guessed_headers = self.guess_headers(self.headers, 
    33                 known_headers) 
     64        self.guessed_headers = self._guess_headers(known_headers) 
    3465        self.rows = tuple(islice(reader, 2)) 
    3566 
    36     def guess_headers(self, csv_headers, known_headers): 
     67    def _guess_headers(self, known_headers): 
    3768        headers = [] 
    38         for header in csv_headers: 
     69        for header in self.headers: 
    3970            h = _to_underscore(header.lower()) 
    4071            if h in known_headers: 
     
    4576 
    4677class CSVImporter(object): 
    47     HEADERS_SET = set().union(*(cls.get_creation_fields() 
    48             for cls in models.get_all_plmobjects().itervalues())) 
    49     HEADERS_SET.add(None) 
    50     HEADERS = sorted(HEADERS_SET) 
    51  
    52     REQUIRED_FIELDS = ("type", "reference", "revision", "name", "group", "lifecycle") 
     78    """ 
     79    Abstract class to import data from a CSV file. 
     80 
     81    :param csv_file: file being imported 
     82    :type csv_file: a file like object 
     83    :param user: user who imports the file 
     84    :type user: :class:`~django.contrib.auth.models.User`  
     85    :param encoding: encoding of the file (`utf-8`, `ascii`, etc.) 
    5386     
    54     @classmethod 
    55     def MISSING_HEADERS_MSG(cls): 
    56         fields = ", ".join(cls.REQUIRED_FIELDS) 
    57         return "Missing headers: %s are required." % fields 
     87    For "end users", this class has two useful methods: 
     88         
     89        * :meth:`get_preview` to generate a :class:`Preview` of the file 
     90        * :meth:`import_csv` to import the csv file 
     91     
     92    An implementation must overwrite the methods :meth:`get_headers_set` and 
     93    :meth:`parse_row` and redefine the attribute :attr:`REQUIRED_HEADERS`. 
     94    """ 
     95 
     96    __metaclass__ = ABCMeta 
     97 
     98    #: Headers that must be present in the csv file 
     99    REQUIRED_HEADERS = () 
    58100 
    59101    def __init__(self, csv_file, user, encoding="utf-8"): 
     
    62104        self.encoding = encoding 
    63105 
     106    @classmethod 
     107    @abstractmethod 
     108    def get_headers_set(cls): 
     109        """ 
     110        Returns a set of all possible headers.  
     111 
     112        .. note:: 
     113 
     114            This method is abstract and must be implemented. 
     115        """ 
     116        return set() 
     117 
     118    @classmethod 
     119    def get_headers(cls): 
     120        """ 
     121        Returns a sorted list of all possible headers. 
     122        """ 
     123        headers = [None] 
     124        headers.extend(sorted(cls.get_headers_set())) 
     125        return headers 
     126 
     127    @classmethod 
     128    def get_missing_headers_msg(cls): 
     129        """ 
     130        Returns a message explaining which headers are required. 
     131        """ 
     132        headers = ", ".join(cls.REQUIRED_HEADERS) 
     133        return u"Missing headers: %s are required." % headers 
     134 
    64135    def get_preview(self): 
     136        """ 
     137        Returns a :class:`Preview` of the csv file. 
     138        """ 
    65139        self.csv_file.seek(0) 
    66         return Preview(self.csv_file, self.encoding, self.HEADERS_SET) 
     140        return Preview(self.csv_file, self.encoding, self.get_headers_set()) 
    67141 
    68142    @transaction.commit_on_success 
    69143    def import_csv(self, headers): 
     144        """ 
     145        Imports the csv file. *headers* is the list of headers as given by the 
     146        user. Columns whose header is `None` are ignored. 
     147        *headers* must contains all values of :attr:`REQUIRED_HEADERS`. 
     148 
     149        If one or several errors occur (missing headers, row which can not be 
     150        parsed), a :exc:`CSVImportError` is raised with all detected errors. 
     151 
     152        :return: A list of :class:`.PLMObjectController` of all created objects. 
     153        """ 
    70154        self.csv_file.seek(0) 
    71155        reader = UnicodeReader(self.csv_file, encoding=self.encoding) 
    72156        self.headers_dict = dict((h, i) for i, h in enumerate(headers)) 
    73157        # checks that required columns are presents 
    74         for field in self.REQUIRED_FIELDS: 
     158        for field in self.REQUIRED_HEADERS: 
    75159            if field not in self.headers_dict: 
    76                 raise CSVImportError({1: self.MISSING_HEADERS_MSG()}) 
     160                raise CSVImportError({1: self.get_missing_headers_msg()}) 
    77161        # read the header 
    78162        reader.next() 
    79         self.errors = defaultdict(ErrorList) 
     163        self._errors = defaultdict(ErrorList) 
    80164        self.objects = [] 
    81165        # parse each row 
    82166        for line, row in enumerate(reader): 
    83167            try: 
    84                 self.parse_row(line, row) 
     168                self.parse_row(line + 2, row) 
    85169            except Exception, e: 
    86                 self.errors[line + 2].append(unicode(e)) 
    87         if self.errors: 
    88             raise CSVImportError(self.errors) 
     170                self.store_errors(line + 2, e) 
     171        if self._errors: 
     172            raise CSVImportError(self._errors) 
    89173        for obj in self.objects: 
    90174            obj.unblock_mails() 
    91175        return self.objects 
    92176 
     177    def store_errors(self, line, *errors): 
     178        """ 
     179        Appends *errors* to the list of errors which occurs at the line *line*. 
     180        """ 
     181        for e in errors: 
     182            if isinstance(e, Exception): 
     183                e = unicode(e) 
     184            self._errors[line].append(e) 
     185     
     186    def get_value(self, row, header): 
     187        return row[self.headers_dict[header]] 
     188 
     189    def get_values(self, row, *headers): 
     190        return [self.get_value(row, h) for h in headers] 
     191 
     192    @abstractmethod 
    93193    def parse_row(self, line, row): 
     194        """ 
     195        Method called by :meth:`import_csv` for each row. 
     196 
     197        :param line: line number of current row, useful to store a list of 
     198                     errors 
     199        :type line: int 
     200        :param row: row being parsed. 
     201        :type row: list of unicode strings. 
     202 
     203        This method must be overwritten. Implementation can use the methods 
     204        :meth:`get_value`, :meth:`get_values`, and :meth:`store_errors` to 
     205        retrieve values and store detected errors. 
     206 
     207        .. warning:: 
     208 
     209            All :class:`.Controller` created should not send emails since an 
     210            error may occur and thus, all modifications would be cancelled. 
     211            To block mails, call :meth:`.Controller.block_mails`. You can 
     212            released all blocked mails by appending the controller to 
     213            :attr:`objects`. :meth:`import_csv` will send mails if no errors 
     214            occurred. 
     215 
     216            Example:: 
     217 
     218                ctrl = get_obj(type, reference, revision, user) 
     219                ctrl.block_mails() 
     220                ... 
     221                if ok: 
     222                    self.objects.append(ctrl) 
     223        """ 
     224        pass  
     225 
     226class PLMObjectsImporter(CSVImporter): 
     227    """ 
     228    An :class:`CSVImporter` that creates :class:`PLMObject` from 
     229    a csv file. 
     230 
     231    The CSV must contain the following columns: 
     232 
     233        * type 
     234        * reference 
     235        * revision 
     236        * name 
     237        * group (name of the group, not its id) 
     238        * lifecycle (name of the lifecycle, not its id) 
     239 
     240    Moreover, it must have a column for each required field of defined types. 
     241    """ 
     242 
     243    #: Headers that must be present in the csv file 
     244    REQUIRED_HEADERS = ("type", "reference", "revision", "name", "group", "lifecycle") 
     245 
     246    @classmethod 
     247    def get_headers_set(cls): 
     248        """ 
     249        Returns a set of all possible headers. 
     250        """ 
     251        return set().union(*(cls.get_creation_fields() 
     252            for cls in models.get_all_plmobjects().itervalues())) 
     253 
     254 
     255    def parse_row(self, line, row): 
     256        """ 
     257        Method called by :meth:`import_csv` for each row. 
     258        """ 
    94259        from openPLM.plmapp.forms import get_creation_form 
    95         type_ = row[self.headers_dict["type"]] 
    96         reference = row[self.headers_dict["reference"]] 
    97         revision = row[self.headers_dict["revision"]] 
     260        type_, reference, revision = self.get_values(row, "type", "reference", 
     261            "revision") 
    98262        cls = models.get_all_plmobjects()[type_] 
    99         group = models.GroupInfo.objects.get(name=row[self.headers_dict["group"]]) 
    100         lifecycle = models.Lifecycle.objects.get(name=row[self.headers_dict["lifecycle"]]) 
     263        group = models.GroupInfo.objects.get(name=self.get_value(row, "group")) 
     264        lifecycle = models.Lifecycle.objects.get(name=self.get_value(row, "lifecycle")) 
    101265        form = get_creation_form(self.user, cls) 
    102266        data = { 
    103                 "type" :type_, 
     267                "type" : type_, 
    104268                "group" : str(group.id), 
    105269                "reference" : reference, 
     
    108272        for field in form.fields: 
    109273            if field not in data and field in self.headers_dict: 
    110                 data[field] = row[self.headers_dict[field]] 
     274                data[field] = self.get_value(row, field) 
    111275        form = get_creation_form(self.user, cls, data) 
    112276        if not form.is_valid(): 
    113277            items = (mark_safe(u"%s: %s" % item) for item  
    114278                    in form.errors.iteritems()) 
    115             self.errors[line + 2].extend(items) 
     279            self.store_errors(line, *items) 
    116280        else: 
    117281            obj = PLMObjectController.create_from_form(form, self.user, True) 
     
    120284 
    121285class BOMImporter(CSVImporter): 
    122      
    123     REQUIRED_FIELDS = ("parent-type", "parent-reference", "parent-revision", 
    124                        "child-type", "child-reference", "child-revision", 
    125                        "quantity", "order")  
    126  
    127     HEADERS_SET = set(REQUIRED_FIELDS)  
    128     HEADERS_SET.add(None) 
    129     HEADERS = sorted(HEADERS_SET) 
     286    """ 
     287    A :class:`CSVImporter` that builds a bom from a CSV file. 
     288 
     289    The CSV must contain the following columns: 
     290 
     291        * parent-type 
     292        * parent-reference 
     293        * parent-revision 
     294        * child-type 
     295        * child-reference 
     296        * child-revision 
     297        * quantity 
     298        * order 
     299    """ 
     300 
     301    REQUIRED_HEADERS = ("parent-type", "parent-reference", "parent-revision", 
     302                        "child-type", "child-reference", "child-revision", 
     303                        "quantity", "order")  
     304 
     305    HEADERS_SET = set(REQUIRED_HEADERS) 
     306 
     307    @classmethod 
     308    def get_headers_set(cls): 
     309        return cls.HEADERS_SET  
    130310     
    131311    def parse_row(self, line, row):  
    132312        from openPLM.plmapp.base_views import get_obj 
    133         ptype = row[self.headers_dict["parent-type"]] 
    134         preference = row[self.headers_dict["parent-reference"]] 
    135         prevision = row[self.headers_dict["parent-revision"]] 
     313        ptype, preference, prevision = self.get_values(row, 
     314                *["parent-" + h for h in ("type", "reference", "revision")]) 
    136315        parent = get_obj(ptype, preference, prevision, self.user) 
    137316 
    138         ctype = row[self.headers_dict["child-type"]] 
    139         creference = row[self.headers_dict["child-reference"]] 
    140         crevision = row[self.headers_dict["child-revision"]] 
     317        ctype, creference, crevision = self.get_values(row, 
     318                *["child-" + h for h in ("type", "reference", "revision")]) 
    141319        child = get_obj(ctype, creference, crevision, self.user) 
    142320 
     
    146324        self.objects.append(child) 
    147325 
    148         quantity = float(row[self.headers_dict["quantity"]]) 
    149         order = float(row[self.headers_dict["order"]]) 
     326        qty = self.get_value(row, "quantity").replace(",", ".").replace(" ", "") 
     327        quantity = float(qty) 
     328        order = int(self.get_value(row, "order").replace(" ", "")) 
    150329 
    151330        parent.add_child(child, quantity, order) 
    152331    
    153  
    154 IMPORTERS = {"csv" : CSVImporter, "bom" : BOMImporter } 
    155  
     332#: Dictionary (name -> CSVImporter's subclass) of known :class:`CSVImporter` 
     333IMPORTERS = {"csv" : PLMObjectsImporter, "bom" : BOMImporter } 
     334 
  • trunk/openPLM/plmapp/forms.py

    r405 r406  
    468468def get_headers_formset(Importer): 
    469469    class CSVHeaderForm(forms.Form): 
    470         header = forms.TypedChoiceField(choices=zip(Importer.HEADERS, 
    471             Importer.HEADERS), required=False) 
     470        HEADERS = Importer.get_headers() 
     471        header = forms.TypedChoiceField(choices=zip(HEADERS, HEADERS), 
     472                required=False) 
    472473 
    473474    class BaseHeadersFormset(BaseFormSet): 
     
    482483                    raise forms.ValidationError(_("Columns must have distinct headers.")) 
    483484                headers.append(header)  
    484             for field in Importer.REQUIRED_FIELDS: 
     485            for field in Importer.REQUIRED_HEADERS: 
    485486                if field not in headers: 
    486                     raise forms.ValidationError(Importer.MISSING_HEADERS_MSG()) 
     487                    raise forms.ValidationError(Importer.get_missing_headers_msg()) 
    487488            self.headers = headers 
    488489 
    489  
    490490    return formset_factory(CSVHeaderForm, extra=0, formset=BaseHeadersFormset) 
    491491 
  • trunk/openPLM/plmapp/tests/csvimport.py

    r405 r406  
    8383        UnicodeWriter(csv_file).writerows(csv_rows) 
    8484        csv_file.seek(0) 
    85         importer = CSVImporter(csv_file, self.user) 
     85        importer = PLMObjectsImporter(csv_file, self.user) 
    8686        headers = importer.get_preview().guessed_headers 
    8787        objects = importer.import_csv(headers) 
     
    102102        UnicodeWriter(csv_file).writerows(csv_rows) 
    103103        csv_file.seek(0) 
    104         importer = CSVImporter(csv_file, self.user) 
     104        importer = PLMObjectsImporter(csv_file, self.user) 
    105105        headers = importer.get_preview().guessed_headers 
    106106        self.assertRaises(CSVImportError, importer.import_csv, headers) 
Note: See TracChangeset for help on using the changeset viewer.