From: olivier Date: Fri, 28 Mar 2008 16:52:28 +0000 (+0000) Subject: ajout specification de conversion des donnees CREM, en cours d'ecriture X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=b996c17cdf015a59c8110c36d8ebaeaf33451bc9;p=telemeta-data.git ajout specification de conversion des donnees CREM, en cours d'ecriture git-svn-id: http://svn.parisson.org/svn/crem@4 3bf09e05-f825-4182-b9bc-eedd7160adf0 --- diff --git a/spec/build.sh b/spec/build.sh new file mode 100644 index 0000000..14af7e3 --- /dev/null +++ b/spec/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +python parse.py +cd build +rst2html docref.txt docref.html && rst2latex docref.txt docref.tex && pdflatex docref.tex + diff --git a/spec/docref.txt b/spec/docref.txt new file mode 100644 index 0000000..48bb608 --- /dev/null +++ b/spec/docref.txt @@ -0,0 +1,223 @@ + + +=================================================================== +Recommandations pour la réorganisation de la base de donnée du CREM +=================================================================== +------------------------------------ +Document de référence, version alpha +------------------------------------ + + +I - Enjeux +========== + +I.1 - Recherche d'informations +------------------------------ + +L'analyse de la structure de données et la rencontre des personnes concernées par son usage +permettent de délimiter des enjeux majeurs. Il apparaît notamment que le fond documentaire +que constitue cette base n'est actuellement pas mis à profit comme il pourrait l'être. Cette +base est largement sous-utilisée, les chercheurs, pour certains, ne l'utilisent pas du tout. + +La raison principale de cette situation est le fait que l'environnement technique et la +structure de la base ont été conçus pour s'inscrire dans un environnement documentaire +traditionnel, qui implique généralement de prendre contact avec un documentaliste pour effectuer +une recherche. Le documentaliste est un spécialiste de la base, il possède les connaissances +techniques nécessaires à sa consultation. + +Mais à l'heure d'Internet, ce processus de recherche est trop lourd et inadapté, en un mot +: obsolète. Les chercheurs lui préfèrent d'autres fonds documentaires accessibles en ligne, +de façon immédiate, et équipés d'outils de recherche puissants et ergonomiques. + +L'enjeu principal de la réorganisation de la base du CREM consiste donc à : sous-tendre le +développement d'un outil moderne de gestion et de consultation en ligne, avec des fonctions +de recherche puissantes et intuitives déterminer et améliorer les données de la base qui +sont prioritaires pour la recherche d'information + +En de-ça des-dits champs prioritaires, il existe un grand nombre d'information techniques de +second ordre, qui soit ne sont utiles qu'aux documentalistes, soit ne présentent pas d'intérêt +majeur pour la recherche d'information. Ces informations peuvent être conservées telles quelles, +sans grand effort de réorganisation, et resteront ainsi disponibles lors de la consultation +individuelle des fiches. + +En procédant de cette façon, c'est à dire en délimitant les-dîtes priorités, il semble +possible de dégager une véritable plus-value, de rendre le fond documentaire du CREM plus vivant, +sans pour autant engager des moyens démesurés dans la réorganisation de la base de donnée. + +I.2 - Gestion et mise à jour des données +---------------------------------------- + +Il est entendu qu'au delà de la consultation d'information, il s'agit également de faciliter +la gestion des données, leur mise à jour. Cette question implique pour une large part des +problématiques d'ordre ergonomique, liées au développement d'un outil logiciel autour de la +base, ce qui n'entre pas dans le cadre de la réorganisation de la base de données. + +Cependant, au niveau de la base, certains choix sont structurant. Par exemple l'emploi d'une +liste hiérarchisée de lieux telle que le Thesaurus of Geographical Names (TGN, recommandé par +Dublin Core), permet, en choisissant une région, une sous-région ou un village, de renseigner +automatiquement le pays (et les autres zones géographiques intermédiaires), ce qui diminue +l'effort et prévient les erreurs de saisie. + +Pour un grand nombre de champs, l'emploi d'énumérations (liste de valeurs valides pour +un champ donné) facilite et valide déjà la mise à jour des données. Cet aspect de la base +devra être conservé. + +Cependant, il semble que l'enjeu majeur de la réorganisation, du point de vue de la gestion +des données, est de permettre un travail collaboratif, entre chercheurs et documentalistes. C'est +là, d'une manière générale, ce qui permet sur Internet de rendre un fond documentaire vivant. + +Dans le cadre du CREM, il serait ainsi idéal d'amener les chercheurs à alimenter la base de +façon autonome, en sus des documentalistes dont c'est le métier. Ce serait là une véritable +nouveauté, représentant un fort potentiel d'enrichissement des données. + +D'une façon générale, une application en ligne ("webifiée") bien conçue se prête +très bien au travail collaboratif. Cependant, il semble que dans notre cas, les potentielles +contributions des chercheurs se heurtent à une question importante de confidentialité et +de propriété intellectuelle. Des cas ont en effet été rapportés d'usage détournés du +fruit du travail du centre de recherche, et d'une manière générale, il semble qu'il faille +observer une grande prudence pour la mise en commun d'enregistrement sonores et autres données, +qui représentent une matière précieuse aux yeux des chercheurs. + +La sécurité informatique est pour une large part distinct de la base de donnée ; elle +implique des efforts particuliers au niveau du développement applicatif, et de l'administration +de l'infrastructure réseau. + +Cependant, au niveau de la base de donnée, il est possible de mettre l'accent sur la +propriété des données. Chaque ressource (collection, item) peut en effet se voir attribuer +un propriétaire. Le dit propriétaire devrait ensuite pouvoir choisir quel autre utilisateur +et/ou groupe d'utilisateur est autorisé à consulter, écouter, et/ou modifier la ressource. + +En rassurant ainsi les chercheurs, il est envisageable que la base s'enrichisse notablement +par leurs contributions, dont certaines d'abord privées, pourront, à leur guise, devenir +petit à petit accessibles à un nombre grandissant d'utilisateurs et de groupes de travail. + +I.3 - Données prioritaires + +Après consultation des différents utilisateurs il apparaît que les meta-données principales +pour chaque item sont : + +* l'identifiant unique de l'item +* le titre de l'item +* la zone géographique +* le nom du collecteur, c'est à dire le chercheur qui a collecté cet enregistrement +* les instruments de musique utilisés +* l'année d'enregistrement +* le contexte ethnographique +* l'ethnie + +Il s'agit d'apporter une attention particulière à celles-ci, notamment en les normalisant, +pour faciliter la recherche d'informations, mais aussi pour l'ajout et la gestion simplifiés +d'items. + +Les autres données pourront être conservées telles quelles sans grand effort de normalisation. + +I.4 - Dublin Core +----------------- + +... + +II - Modalités de conversion/réorganisation de la base +====================================================== + +II.1 - Enumérations simples +--------------------------- + +dynamic:tables.describe_enums() + +II.2 - Collections +------------------ + +Ancien nom de table : support + +Nouveau nom de table : media_collections + +II.2.1 - Champs convertis +~~~~~~~~~~~~~~~~~~~~~~~~~ + +dynamic:tables.media_collections.describe_conversion() + +II.2.2 - Nouveaux champs +~~~~~~~~~~~~~~~~~~~~~~~~ + +dynamic:tables.media_collections.describe_new_fields() + +II.2.3 - Champs supprimés +~~~~~~~~~~~~~~~~~~~~~~~~~ + +dynamic:tables.media_collections.describe_removed_fields() + +II.3 - Items +------------ + +Ancien nom de table : phono + +Nouveau nom de table : media_items + +II.3.1 - Champs convertis +~~~~~~~~~~~~~~~~~~~~~~~~~ + +dynamic:tables.media_items.describe_conversion() + +II.3.2 - Nouveaux champs +~~~~~~~~~~~~~~~~~~~~~~~~ + +dynamic:tables.media_items.describe_new_fields() + +II.3.3 - Champs supprimés +~~~~~~~~~~~~~~~~~~~~~~~~~ + +dynamic:tables.media_items.describe_removed_fields() + +II.4 - Sélections +----------------- + +II.4.1 - Liste +~~~~~~~~~~~~~~ + +Nom de table: +dynamic:tables.media_playlists.name + +dynamic:tables.media_playlists.describe_new_fields() + +II.4.2 - Table de relation interne +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nom de table: +dynamic:tables.media_playlist_resources.name + +dynamic:tables.media_playlist_resources.describe_new_fields() + +II.5 - Thesaurus Géographique +----------------------------- + +II.5.1 - Lieux +~~~~~~~~~~~~~~ + +Nom de table: +dynamic:tables.locations.name + +dynamic:tables.locations.describe_new_fields() + +II.5.2 - Alias des lieux +~~~~~~~~~~~~~~~~~~~~~~~~ + +Nom de table: +dynamic:tables.location_aliases.name + +dynamic:tables.location_aliases.describe_new_fields() + +II.5.3 - Relations hiérarchiques +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Nom de table: +dynamic:tables.location_relations.name + +dynamic:tables.location_relations.describe_new_fields() + +II.6 - Utilisateurs +------------------- + +Nom de table: +dynamic:tables.users.name + +dynamic:tables.users.describe_new_fields() diff --git a/spec/elements.py b/spec/elements.py new file mode 100644 index 0000000..e46b8d7 --- /dev/null +++ b/spec/elements.py @@ -0,0 +1,136 @@ +#coding: utf-8 +import sqlalchemy +import texttable + +def make_table(rows, width=20): + return texttable.indent(rows, hasHeader = True, separateRows = True, + prefix=u'| ', postfix=u' |', + wrapfunc = lambda x: texttable.wrap_onspace(x, width=width)) + +def describe_enums(metadata): + rows = [['Nom interne', 'Ancien nom', 'Description']] + for enum in metadata.enums: + rows.append([enum.name, enum.old_name, enum.label]) + return make_table(rows, 40) + +class Column(sqlalchemy.Column): + old_name = '' + label = '' + desc = '' + dc = '' + comment = '' + conversion = '' + enum = None + + def __init__(self, name, type, *args, **kwargs): + parent_args = {} + for key, value in kwargs.items(): + if key == 'old_name': + if isinstance(value, list): + self.old_name = unicode(",\n".join(value), 'utf-8') + #self.old_name = [] + #for item in value: + # self.old_name.append(unicode(item, 'utf-8')) + else: + self.old_name = unicode(value, 'utf-8') + elif key == 'label': + self.label = unicode(value, 'utf-8') + elif key == 'desc': + self.desc = unicode(value, 'utf-8') + elif key == 'dc': + self.dc = value + elif key == 'comment': + self.comment = unicode(value, 'utf-8') + elif key == 'conversion': + self.conversion = unicode(value, 'utf-8') + else: + parent_args[key] = value + + if isinstance(type, Enumeration): + args = (sqlalchemy.ForeignKey(type.name + '.id'),) + args + self.enum = type + type = sqlalchemy.Integer + + super(Column, self).__init__(name, type, *args, **parent_args) + +class RemovedColumn(object): + old_name = '' + comment = '' + + def __init__(self, old_name, comment=''): + self.old_name = unicode(old_name, 'utf-8') + self.comment = unicode(comment, 'utf-8') + +class Table(sqlalchemy.Table): + + def __init__(self, table_name, metadata, label, *args): + self.label = unicode(label, 'utf-8') + real_columns = [] + self.removed_columns = [] + for column in args: + if isinstance(column, RemovedColumn): + self.removed_columns.append(column) + else: + real_columns.append(column) + super(Table, self).__init__(table_name, metadata, *real_columns) + + + def describe_new_fields(self): + rows = [['Nom', 'Nom interne', 'Dublin Core']] + for column in self.columns: + if not column.old_name: + rows.append([column.label, column.name, column.dc]) + + return make_table(rows) + + def describe_conversion(self): + rows = [['Nouveau nom', 'Ancien nom', 'Nouveau nom interne', 'Dublin Core']] + for column in self.columns: + if column.old_name: + rows.append([column.label, column.old_name, column.name, column.dc]) + + return make_table(rows) + + def describe_removed_fields(self): + rows = [['Nom', 'Commentaire']] + for column in self.removed_columns: + rows.append([column.old_name, column.comment]) + + return make_table(rows, 50) + + def to_dot(self): + dot = u'digraph g {\n' + dot += ' charset = "utf-8";\n' + dot += ' node [shape=record, charset="utf-8"];\n' + dot += ' rankdir = LR;\n' + dot += ' subgraph cluster_new_fields {\n' + dot += ' label = "Nouveaux champs";\n' + dot += ' color = black;\n' + old_fields = '' + conversion = '' + for column in self.columns: + dot += ' ' + column.name + '[label = "{' + column.label + ' | ' + column.name + '}"];\n' + if column.old_name: + old_fields += ' old_' + column.name + '[label = "' + column.old_name + '"];\n' + conversion += ' old_' + column.name + ' -> ' + column.name + ';\n' + dot += ' }\n' + dot += ' subgraph cluster_old_fields {\n' + dot += ' label = "Anciens champs";\n' + dot += ' color = black;\n' + dot += old_fields + dot += ' }\n' + dot += conversion + dot += '}\n' + return dot + +class Enumeration(Table): + + def __init__(self, name, metadata, label='', old_name=''): + self.old_name = unicode(old_name, 'utf-8') + if not hasattr(metadata, 'enums'): + metadata.enums = [] + metadata.enums.append(self) + super(Enumeration, self).__init__(name, metadata, label, + Column('id', sqlalchemy.Integer, primary_key=True), + Column('value', sqlalchemy.String(250), unique=True) + ) diff --git a/spec/parse.py b/spec/parse.py new file mode 100644 index 0000000..535832c --- /dev/null +++ b/spec/parse.py @@ -0,0 +1,18 @@ + +import tables +import codecs +input = codecs.open('docref.txt', 'r', "utf-8") +output = codecs.open('build/docref.txt', 'w', "iso-8859-1") + +for line in input: + if line[0:8] == 'dynamic:': + output.write(eval(line[8:], globals(), locals()) + "\n") + else: + output.write(line) + +input.close() +output.close() + +output = codecs.open('build/collections.dot', 'w', 'utf-8') +output.write(tables.media_collections.to_dot()) +output.close() diff --git a/spec/staticenum.py b/spec/staticenum.py new file mode 100644 index 0000000..d364f5b --- /dev/null +++ b/spec/staticenum.py @@ -0,0 +1,69 @@ +#!python + +## The MIT License + +## Copyright (c) + +## Permission is hereby granted, free of charge, to any person obtaining a copy +## of this software and associated documentation files (the "Software"), to deal +## in the Software without restriction, including without limitation the rights +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +## copies of the Software, and to permit persons to whom the Software is +## furnished to do so, subject to the following conditions: + +## The above copyright notice and this permission notice shall be included in +## all copies or substantial portions of the Software. + +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +## THE SOFTWARE. + + +from sqlalchemy import types, exceptions + +class StaticEnum(types.TypeDecorator): + impl = types.Unicode + + def __init__(self, values, empty_to_none=False, strict=False): + """Emulate an Enum type. + + values: + A list of valid values for this column + empty_to_none: + Optional, treat the empty string '' as None + strict: + Also insist that columns read from the database are in the + list of valid values. Note that, with strict=True, you won't + be able to clean out bad data from the database through your + code. + """ + + if values is None or len(values) is 0: + raise exceptions.AssertionError('StaticEnum requires a list of values') + self.empty_to_none = empty_to_none + self.strict = strict + self.values = values[:] + + # The length of the string/unicode column should be the longest string + # in values + size = max([len(v) for v in values if v is not None]) + super(StaticEnum, self).__init__(size) + + + def convert_bind_param(self, value, engine): + if self.empty_to_none and value is '': + value = None + if value not in self.values: + raise exceptions.AssertionError('"%s" not in StaticEnum.values' % value) + return super(StaticEnum, self).convert_bind_param(value, engine) + + + def convert_result_value(self, value, engine): + if self.strict and value not in self.values: + raise exceptions.AssertionError('"%s" not in StaticEnum.values' % value) + return super(StaticEnum, self).convert_result_value(value, engine) + diff --git a/spec/tables.py b/spec/tables.py new file mode 100644 index 0000000..41fd6d4 --- /dev/null +++ b/spec/tables.py @@ -0,0 +1,331 @@ +#coding: utf-8 + +import sqlalchemy +from sqlalchemy import String, Integer, ForeignKey, Time, Date, MetaData, Text, Boolean +from elements import Column, RemovedColumn, Table, Enumeration +import elements +from staticenum import StaticEnum + +metadata = MetaData() +def describe_enums(): + return elements.describe_enums(metadata) + +# Users + +users = Table('users', metadata, 'Utilisateurs', + Column('username', String(250), primary_key=True, label='Nom d\'utilisateur'), + Column('level', StaticEnum('user,maintainer,administrator'), label='Niveau de permissions'), + Column('first_name', String(250), label='Prénom'), + Column('last_name', String(250), label='Nom'), + Column('phone', String(250), label='Téléphone'), + Column('email', String(250), label='E-Mail') +) + +# Playlists + +media_playlists = Table('playlists', metadata, 'Sélections personelles d\'items et de collections', + Column('id', Integer, primary_key='true', label='Identifiant'), + Column('owner_username', String(250), ForeignKey('users.username'), label='Propriétaire'), + Column('name', String(250), label='Intitulé')) + +media_playlist_resources = Table('playlist_resources', metadata, 'Ressources associées aux sélections personelles ', + Column('playlist_id', Integer, ForeignKey('playlists.id'), label='Identifiant de la sélection'), + Column('resource_type', StaticEnum('item', 'collection'), label='Type de ressource (item, collection)'), + Column('resource_id', String(250), label='Identifiant de la ressource') +) + +# Simple enumerations + +physical_formats = Enumeration('physical_formats', metadata, 'Formats physiques', old_name='Format (e)') +publishing_status = Enumeration('publishing_status', metadata, 'Status d\'édition/réédition', old_name='Réédition (e)') +publishers = Enumeration('publishers', metadata, 'Editeurs', old_name='Editeur1 (e)') +acquisition_modes = Enumeration('acquisition_modes', metadata, 'Modes d\'acquisition', old_name='Mode_Acqui (e)') +metadata_authors = Enumeration('record_authors', metadata, 'Rédacteurs des fiches', old_name='Rédacteur_Fiche (e)') +metadata_writers = Enumeration('metadata_writers', metadata, 'Opérateur de saisie des fiches', old_name='Saisie_Fiche (e)') +legal_rights = Enumeration('legal_rights', metadata, 'Statuts juridiques', old_name='Droit_d\'Utiliser (e)') +recording_contexts = Enumeration('recording_contexts', metadata, 'Contextes d\'enregistrement', old_name='Terrain_ou_Autre (e)') +ad_conversions = Enumeration('ad_conversions', metadata, 'Statuts de numérisation', old_name='Numérisation (e)') +ethnic_groups = Enumeration('ethnic_groups', metadata, 'Ethnies/Groupe social', old_name='Ethnie (t)') +vernacular_styles = Enumeration('vernacular_styles', metadata, 'Forme / genre vernaculaire', old_name='Form (t)') +generic_styles = Enumeration('generic_styles', metadata, 'Forme / genre générique', old_name='FormStyle générique (e)') +context_keywords = Enumeration('context_keywords', metadata, 'Mots clés du contexte ethnographique', old_name='Mot_Clef (t)') +publisher_collections = Enumeration('publisher_collections', metadata, 'Collections éditeur', old_name='Collection_Série (e)') + +# Geographic Thesaurus + +location_types = Enumeration('location_types', metadata, 'GeoEthno / Types de lieux') + +locations = Table('locations', metadata, 'GeoEthno / Lieux)', + Column('name', String(127), primary_key=True, label='Terme descripteur'), + Column('type', StaticEnum('country', 'continent', 'other')), + Column('complete_type', location_types), + Column('current_name', String(127), ForeignKey('locations.name'), label='Nom actuel'), + Column('is_authoritative', Boolean), +) + +location_aliases = Table('location_aliases', metadata, 'GeoEthno / Alias des lieux', + Column('location_name', String(127), ForeignKey('locations.name'), primary_key=True), + Column('alias', String(127), primary_key=True), + Column('is_authoritative', Boolean)) + +location_relations = Table('location_relations', metadata, 'GeoEthno / Relations hiérachiques', + Column('location_name', String(127), ForeignKey('locations.name'), primary_key=True), + Column('parent_location_name', String(127), ForeignKey('locations.name'), primary_key=True) +) + +# Media Collections + +media_collections = Table('media_collections', metadata, 'Collections', + Column('reference', + String(250), unique=True, + old_name='Réf', label='Référence'), + Column('physical_format_id', + physical_formats, + old_name='Format', label='Format', + desc="Format du 1er exemplaire archivé"), + Column('old_id', + String(250), + old_name='Cote', label='Ancienne cote'), + Column('id', + String(250), primary_key=True, + dc='identifier', + label='Cote', conversion='à préciser'), + Column('title', + String(250), + dc='title', + old_name='Titre', label='Titre'), + Column('native_title', + String(250), + dc='title', + old_name='Transcrip_Trad', label='Traduction du titre'), + Column('physical_items_num', + Integer, + old_name='Nb_de_pieces', label='Nombre de supports physiques'), + Column('publishing_status_id', + publishing_status, + old_name='Réédition', label='Réédition'), + RemovedColumn(old_name='Original'), + RemovedColumn(old_name='Copie_TotPartie'), + RemovedColumn(old_name='Copié_de'), + Column('creator', + String(250), + dc='creator', + old_name='Auteur_Compil', label='Auteur / Cédant'), + Column('booklet_author', + String(250), + dc='contributor', + old_name='Auteur_Notice', label='Auteur notice'), + Column('booklet_description', + Text, + old_name='Notice', label='Notice / Dossier technique'), + Column('collector', + Text, + dc='contributor', + old_name='Collecteur', label='Collecteur'), + Column('publisher_id', + publishers, + dc='publisher', + old_name='Editeur', label='Editeur', + desc='Pour les documents ÉDITÉS:Nom et État de l\'Editeur. Pour les INÉDITS :voir champ "Type de document".'), + Column('year_published', + Integer, + dc='date', + old_name='Année_parution', label='Année de parution', + desc='Ne concerne que les documents ÉDITÉS.', + conversion='à préciser, traiter les nombres négatifs ?'), + Column('publisher_collection', + publisher_collections, + old_name='Collect_Série', label='Collection éditeur', + comment='faux: nom de la collection, suivi du n° dans la collection.'), + Column('publisher_serial', + String(250), + old_name='Num_Dans_Collec', label='Numéro de série', + desc='Numéro de série dans la collection éditeur', + comment='à valider'), + Column('external_references', + Text, + old_name='Réf_Biblio', label='Bibliographie', + desc='Références biblio/disco/filmographiques, uniquement liées à ce support'), + Column('acquisition_mode_id', + acquisition_modes, + old_name='Mod_Acqui', label='Mode d\'acquisition'), + Column('comment', + Text, + old_name='Commentaire', label='Commentaire'), + Column('metadata_author_id', + metadata_authors, + dc='contributor', + old_name='Rédacteur_Fiche', label='Rédacteur fiche', + desc='Responsable de l\'analyse documentaire'), + Column('metadata_writer_id', + metadata_writers, + old_name='Saisie_Fiche', label='Saisie fiches', + desc='Personne qui a saisi les fiches dans la base de données.'), + Column('legal_rights_id', + legal_rights, + dc='rights', + old_name='Droit_Utiliser', label='Statut juridique'), + Column('alt_ids', + String(250), + old_name='Autres_Cotes', label='Autres exemplaires'), + Column('recorded_from_year', + Integer, + dc='date', + old_name='Année_Enreg', label='Années d\'enregistrement', + conversion="split"), + Column('recorded_to_year', + Integer, + dc='date', + old_name='Année_Enreg', label='Années d\'enregistrement', + conversion="split"), + Column('recording_context_id', + recording_contexts, + old_name='Terrain_ou_Autr', label='Contexte d\'enregistrement'), + Column('approx_duration', + Time(), + old_name='Durée_approx', label='Durée approximative'), + Column('doctype_code', + Integer, + old_name='Tri_DiBm', label='Type de document'), + Column('travail', + String(250), + old_name='Travail', label='?'), + Column('state', + String(250), + old_name='Compil_Face_Plage', label="Etat"), + Column('cnrs_contributor', + String(250), + old_name='Déposant_CNRS', label="Déposant CNRS", + desc='Pour les INÉDITS uniquement. Signale les collectes ayant bénéficées d\'une aide du CNRS.'), + Column('items_done', + String(250), + old_name='Fiches', label='Fiches faîtes', + desc="Signale que les fiches Item de ce support sont présentes dans la base."), + Column('a_informer_07_03', + String(250), + old_name='A informer_07-03', label='?'), + Column('ad_conversion_id', + ad_conversions, + old_name='Numérisation', label="Historique de numérisation"), + RemovedColumn(old_name='Champ36'), +) + +# Media Items + +media_item_context_keywords = Table('media_item_context_keywords', metadata, + 'Mots clés associés à un item', + Column('media_item_id', Integer, ForeignKey('media_items.id')), + Column('context_keyword_id', context_keywords)) + +media_items = Table('media_items', metadata, 'Items', + RemovedColumn(old_name='Réf', comment='Calculé en temps réel à partir de la collection'), + Column('collection_id', + String(250), ForeignKey('media_collections.id'), + dc='relation/isPartOf', + label='Collection'), + RemovedColumn(old_name='Format', comment='Calculé en temps réel à partir de la collection'), + RemovedColumn(old_name='Cote_Support', comment='Calculé en temps réel à partir de la collection'), + Column('track', + String(250), + old_name="Face_Plage", label="N° de l'item"), + Column('id', + String(250), + old_name='Cote_Phono', label="Cote item"), + Column('approx_duration', + Time(), + old_name='Durée', label='Durée'), + Column('recorded_from_date', + Date(), + dc='date', + old_name='Date_enregistr', label='Date', desc='Date de l\'enregistrement'), + Column('recorded_to_date', + Date(), + dc='date', + old_name='Date_enregistr', label='Date'), + Column('location_name', + Integer, ForeignKey('locations.name'), + dc='coverage', + old_name=['Continent', 'Etat', 'Région_Village'], label='Lieu', + desc='(?) Lieu de provenance de la musique. Si le lieu de l\'enregistrement est autre, l\'indiquer en "Remarques".'), + Column('location_comment', + String(250), + old_name='Région_Village', label='Commentaire lieu'), + Column('ethnic_group_id', + ethnic_groups, + old_name='Ethnie_GrSocial', label='Ethnie/Groupe social', comment='attention alias ethnies'), + RemovedColumn(old_name='Titre_Support', comment='Calculé en temps réel à partir de la collection'), + Column('title', + String(250), + dc='title', + old_name='Titre_pièce', label='Titre Item'), + Column('native_title', + String(250), + dc='title', + old_name='Transcrip_Trad', label='Traduction du titre', + desc='Traduction des langues non communes, plus translittération des langues n\'utilisant pas l\'alphabet latin.'), + Column('author', + String(250), + dc='creator', + old_name='Auteur', label='Auteur', + desc='Le cas échéant, faire suivre le nom par une mention "-auteur /-compositeur / -arrangeur"'), + Column('vernacular_style_id', + vernacular_styles, + old_name='Form_Genr_Style', label='Forme / genre vernaculaire [nom à revoir]', + desc='Nom local de la forme ou du genre'), + RemovedColumn(old_name='Struct_Modale'), + RemovedColumn(old_name='Struct_Rythm'), + RemovedColumn(old_name='Struct_Rythm'), + RemovedColumn(old_name='Fonction_Usage', + comment='Champ inutile, les mots clés sont associés via une table de relation externe'), + Column('context_comment', + Text, + old_name='Comm_FonctUsage', label='Contexte ethnographique : commentaires'), + Column('external_references', + String(250), + old_name='Documentation', label='Références', + desc='Références directement liées à l\'item.'), + Column('moda_execut', + String(250), + old_name='Moda_Execut', comment='à supprimer ?'), + Column('copied_from_item_id', + String(250), ForeignKey('media_items.id'), + dc='relation/isVersionOf', + old_name='Copie_de', label='Copie de'), + Column('collector', + String(250), + dc='contributor', + old_name='Enregistré_par', label='Collecteur'), + Column('cultural_area', + String(250), + old_name='Aire culturelle', label='Aire culturelle'), + RemovedColumn(old_name='Année_Enreg', + comment='calculé en temps-réel à partir de la date d\'enregistrement'), + Column('generic_style', + generic_styles, + old_name='FormStyl généri', label='Forme / genre générique'), + Column('collector_selection', + String(250), + old_name='ChoixCollecteur', label='Choix du collecteur', + desc='Permet au collecteur de repérer les items les plus intéressants'), + RemovedColumn(old_name='Repère_bande'), + Column('creator_reference', + String(250), + old_name='NroBandNroPièc', label='Référence du déposant'), +) + + + + + + + + + + + + + + + + + diff --git a/spec/texttable.py b/spec/texttable.py new file mode 100644 index 0000000..3d07d25 --- /dev/null +++ b/spec/texttable.py @@ -0,0 +1,155 @@ +#coding: utf-8 +import cStringIO,operator + +def indent(rows, hasHeader=False, headerChar=u'-', delim=u' | ', justify=u'left', + separateRows=False, prefix=u'', postfix=u'', wrapfunc=lambda x:x): + """Indents a table by column. + - rows: A sequence of sequences of items, one sequence per row. + - hasHeader: True if the first row consists of the columns' names. + - headerChar: Character to be used for the row separator line + (if hasHeader==True or separateRows==True). + - delim: The column delimiter. + - justify: Determines how are data justified in their column. + Valid values are 'left','right' and 'center'. + - separateRows: True if rows are to be separated by a line + of 'headerChar's. + - prefix: A string prepended to each printed row. + - postfix: A string appended to each printed row. + - wrapfunc: A function f(text) for wrapping text; each element in + the table is first wrapped by this function.""" + # closure for breaking logical rows to physical, using wrapfunc + def rowWrapper(row): + newRows = [wrapfunc(item).split('\n') for item in row] + return [[substr or '' for substr in item] for item in map(None,*newRows)] + # break each logical row into one or more physical ones + logicalRows = [rowWrapper(row) for row in rows] + # columns of physical rows + columns = map(None,*reduce(operator.add,logicalRows)) + # get the maximum of each column by the string length of its items + maxWidths = [max([len(item) for item in column]) for column in columns] + rowSeparator = u"+" + u"+".join([u'-' * (width + len(delim) - 1) for width in maxWidths]) + u"+" + headerSep = u"+" + u"+".join([u'=' * (width + len(delim) - 1) for width in maxWidths]) + u"+" + #headerChar * (len(prefix) + len(postfix) + sum(maxWidths) + \ + # len(delim)*(len(maxWidths)-1)) + #headerSep = u"=" * (len(prefix) + len(postfix) + sum(maxWidths) + \ + # len(delim)*(len(maxWidths)-1)) + # select the appropriate justify method + #justify = {'center':str.center, 'right':str.rjust, 'left':str.ljust}[justify.lower()] + output=u'' + if separateRows: + output += rowSeparator + "\n" + + for physicalRows in logicalRows: + for row in physicalRows: + output += \ + prefix \ + + delim.join([item.ljust(width) for (item,width) in zip(row,maxWidths)]) \ + + postfix + "\n" + if hasHeader: output += headerSep + "\n"; hasHeader=False + elif separateRows: output += rowSeparator + "\n" + return output + +# written by Mike Brown +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061 +def wrap_onspace(text, width): + """ + A word-wrap function that preserves existing line breaks + and most spaces in the text. Expects that existing line + breaks are posix newlines (\n). + """ + return reduce(lambda line, word, width=width: '%s%s%s' % + (line, + ' \n'[(len(line[line.rfind('\n')+1:]) + + len(word.split('\n',1)[0] + ) >= width)], + word), + text.split(' ') + ) + +import re +def wrap_onspace_strict(text, width): + """Similar to wrap_onspace, but enforces the width constraint: + words longer than width are split.""" + wordRegex = re.compile(r'\S{'+str(width)+r',}') + return wrap_onspace(wordRegex.sub(lambda m: wrap_always(m.group(),width),text),width) + +import math +def wrap_always(text, width): + """A simple word-wrap function that wraps text on exactly width characters. + It doesn't split the text in words.""" + return '\n'.join([ text[width*i:width*(i+1)] \ + for i in xrange(int(math.ceil(1.*len(text)/width))) ]) + +if __name__ == '__main__': + labels = ('First Name', 'Last Name', 'Age', 'Position') + data = \ + '''John,Smith,24,Software Engineer + Mary,Brohowski,23,Sales Manager + Aristidis,Papageorgopoulos,28,Senior Reseacher''' + rows = [row.strip().split(',') for row in data.splitlines()] + + print 'Without wrapping function\n' + print indent([labels]+rows, hasHeader=True) + # test indent with different wrapping functions + width = 10 + for wrapper in (wrap_always,wrap_onspace,wrap_onspace_strict): + print 'Wrapping function: %s(x,width=%d)\n' % (wrapper.__name__,width) + print indent([labels]+rows, hasHeader=True, separateRows=True, + prefix='| ', postfix=' |', + wrapfunc=lambda x: wrapper(x,width)) + + # output: + # + #Without wrapping function + # + #First Name | Last Name | Age | Position + #------------------------------------------------------- + #John | Smith | 24 | Software Engineer + #Mary | Brohowski | 23 | Sales Manager + #Aristidis | Papageorgopoulos | 28 | Senior Reseacher + # + #Wrapping function: wrap_always(x,width=10) + # + #---------------------------------------------- + #| First Name | Last Name | Age | Position | + #---------------------------------------------- + #| John | Smith | 24 | Software E | + #| | | | ngineer | + #---------------------------------------------- + #| Mary | Brohowski | 23 | Sales Mana | + #| | | | ger | + #---------------------------------------------- + #| Aristidis | Papageorgo | 28 | Senior Res | + #| | poulos | | eacher | + #---------------------------------------------- + # + #Wrapping function: wrap_onspace(x,width=10) + # + #--------------------------------------------------- + #| First Name | Last Name | Age | Position | + #--------------------------------------------------- + #| John | Smith | 24 | Software | + #| | | | Engineer | + #--------------------------------------------------- + #| Mary | Brohowski | 23 | Sales | + #| | | | Manager | + #--------------------------------------------------- + #| Aristidis | Papageorgopoulos | 28 | Senior | + #| | | | Reseacher | + #--------------------------------------------------- + # + #Wrapping function: wrap_onspace_strict(x,width=10) + # + #--------------------------------------------- + #| First Name | Last Name | Age | Position | + #--------------------------------------------- + #| John | Smith | 24 | Software | + #| | | | Engineer | + #--------------------------------------------- + #| Mary | Brohowski | 23 | Sales | + #| | | | Manager | + #--------------------------------------------- + #| Aristidis | Papageorgo | 28 | Senior | + #| | poulos | | Reseacher | + #--------------------------------------------- +