File: tableCSV.py

package info (click to toggle)
slm 2.11-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 24,504 kB
  • sloc: python: 15,349; javascript: 5,043; makefile: 184; sh: 182; xml: 57
file content (345 lines) | stat: -rw-r--r-- 13,692 bytes parent folder | download | duplicates (2)
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
import re
import  csv
import  os
from difflib import SequenceMatcher

from common import safeText

from PyQt6.QtWidgets import QWidget, QCheckBox, QComboBox, QLineEdit, \
    QTableWidgetItem
from PyQt6.QtCore import Qt, pyqtSignal

from Ui_tableCSV import Ui_tableCSV
from Ui_unicity import Ui_unicity

class TableCSV(QWidget, Ui_tableCSV):
    """
    Une table pour interagir avec le contenu d'un fichier CSV

    Paramètres du constructeur
    @parant un tabWidget conteneurt
    @parant mw la fenêtre principale
    """

    # signaux spéciaux
    foreignCells = pyqtSignal(int, bool, list) # on modifie un combo de type
    fcChange = pyqtSignal(int) # on modifie une case Vers... et/ou alias
    
    # les numéros de colonnes
    NOTABLE_COL = 0            # pour les boutons radio de champ notable
    FIRST_EXAMPLE_COL = 1      # pour les trois exemples de valeur
    TYPE_COL = 4               # pour le combo de choix de type
    UNICITY_COL = 5            # pour les boutons radio d'unicité
    VERS_COL = 6               # pour les classes des clés "foreign"
    ALIAS_COL = 7              # alias si plusieurs "foreign" existent

    def __init__(self, parent, mw):
        QWidget.__init__(self, parent)
        Ui_tableCSV.__init__(self)
        self.setupUi(self)
        # connexion des signaux spéciaux
        self.foreignCells.connect(self.manageForeignCells)
        self.fcChange.connect(self.updateData)

        # intialisation diverses
        self.mw = mw         # pointeur vers la fenêtre principale
        self.fieldnames = [] # liste des noms de champs (vertical headers)
        self.filename = ""   # chemin absolu du fichier CSV
        self.modele = ""     # nom du modèle découlant de celui du fichier
        return

    def getConf(self, fieldname, key):
        """
        Récupère une valeur de la configuration
        @param fieldame un nom de champ
        @param key une de ses propriétés
        @return la valeur correspondant à la clé key dans
          self.mw.config["data_CSV"], ce qui converne le tableau local
        """
        if self.filename not in self.mw.config["data_CSV"]:
            self.mw.config["data_CSV"][self.filename] = {}
        if fieldname not in self.mw.config["data_CSV"][self.filename]:
            self.mw.config["data_CSV"][self.filename][fieldname] = {}
        return self.mw.config["data_CSV"][self.filename][fieldname].get(
            key, None)
    
    def populate(self, filename):
        """
        Remplit en grande part le tableau à partir d'un nom de fichier,
        et renseigne par effet de bord les données de configuration
        @param filename chemin absolu du fichier CSV
        """
        self.filename = filename
        dr = csv.DictReader(
            open(filename, "r", encoding="iso-8859-15"), delimiter = ",")
        self.modele = fn2modele(filename)
        self.fieldnames = dr.fieldnames # mémorise les noms de champs localement
        # recopie ces propriétés en les ajoutant à self.tableWidget
        self.tableWidget.filename = self.filename
        self.tableWidget.modele = self.modele
        self.tableWidget.fieldnames = self.fieldnames
        self.tableWidget.setRowCount(len(dr.fieldnames))
        self.tableWidget.setVerticalHeaderLabels(dr.fieldnames)
        # première colonne de faible largeur
        self.tableWidget.setColumnWidth (self.NOTABLE_COL, 30)
        # mise en place des exemples de données lues dans le fichier CSV
        for i in range(3):
            try:
                dic = next(dr)
                for j, (key,val) in enumerate(dic.items()):
                    self.tableWidget.setItem(
                        j, i+self.FIRST_EXAMPLE_COL, QTableWidgetItem(val))
            except StopIteration:
                # s'il y a peu d'exemples, on laisse tomber
                break
        # mise en place de la case à cocher "notable", selon la
        # configuration, sinon cochée s'il y a déjà quelques exemples.
        for j, fn in enumerate(dr.fieldnames):
            notableWidget = QCheckBox("", self)
            isNotable = self.getConf(fn, "notable") == "true"
            checked = Qt.CheckState.Unchecked
            if isNotable:
                checked = Qt.CheckState.Checked
            elif self.getConf(fn, "notable") is None:
                # rien dans la config ...
                # faut-il cocher la case notable quand même ?
                # on regarde si on voit des données parmi les exemples
                notableVal = "false"
                for i in range(3):
                    cell = self.tableWidget.item(j, i+self.FIRST_EXAMPLE_COL)
                    if cell and cell.text():
                        checked = Qt.CheckState.Checked
                        notableVal = "true"
                        break
                self.mw.updateConfig.emit(
                    self.filename, fn, "notable", notableVal)
            notableWidget.setCheckState(checked)
            notableWidget.stateChanged.connect(lambda: self.mw.toDjango.emit())
            self.tableWidget.setCellWidget(j, self.NOTABLE_COL, notableWidget)
        # mise en place des combos pour le choix du type
        for j, fn in enumerate(dr.fieldnames):
            self.tableWidget.setCellWidget(
                j, self.TYPE_COL, TypeWidget(self, j, fn))
        # mise en place des boutons radio d'unicité
        for j, fieldname in enumerate(dr.fieldnames):
            self.tableWidget.setCellWidget(
                j, self.UNICITY_COL, UnicityWidget(self, fieldname))
        return

    def manageForeignCells(self, r, add = True, modeles = []):
        """
        Ajoute des cases pour les clés étrangères dans la ligne numéro r
        du tableau
        @param r Le numéro de ligne
        @param add Vrai [par défaut] quand on veut ajouter les cases
          dans le tableau. Faux quand on veut les supprimer
        @param modeles Une liste des modèles possibles à choisir, à préciser
          quand le paramètre add est vrai.
        """
        t = self.tableWidget
        if add:
            # mise en place du combo en colonne self.VERS_COL
            fieldname = self.fieldnames[r]
            # ajoute un combo de modèles
            modelW = ModelWidget(t, fieldname, modeles)
            modelW.currentIndexChanged.connect(lambda: self.fcChange.emit(r))
            t.setCellWidget(r, self.VERS_COL, modelW)
            cell = t.cellWidget(r, self.VERS_COL)
            foreign = self.getConf(fieldname, "foreign")
            if foreign:
                cell.setCurrentText(foreign)
                self.fcChange.emit(r) # ??? pourquoi ?
            else:
                # c'est un champ encore inconnu de la configuration
                # fait en sorte de deviner le meilleur modèle
                # étant donné le nom du champ
                fn = safeText(fieldname).lower()
                scores = {
                    m : SequenceMatcher(
                        None,
                        safeText(m).lower(), fn).find_longest_match().size for \
                    m in (cell.itemText(i) for i in range(cell.count()))
                }
                # s'il y a au moins 4 lettres en commun, on change de modèle
                best = max(scores.items(), key = lambda x: x[1])
                if best[1] > 3:
                    cell.setCurrentText(best[0])
                    self.mw.updateConfig.emit(
                        self.filename, fieldname, "foreign", best[0])
            # mise en place du lineEdit en colonne self.ALIAS_COL
            edit = QLineEdit("")
            edit.editingFinished.connect(lambda: self.fcChange.emit(r))
            edit.setPlaceholderText("alias ?")
            alias = self.getConf(fieldname, "alias")
            if alias:
                edit.setText(alias)
                self.fcChange.emit(r) # ??? pourquoi ?
            t.setCellWidget(r, self.ALIAS_COL, edit)
        else: # add est faux, on supprimes les cases
            t.removeCellWidget(r, self.VERS_COL)
            t.removeCellWidget(r, self.ALIAS_COL)
        return

    def updateData(self, r):
        """
        Mise à jour de la portion de configuration pointée par
        self.data pour prendre en compte le contenu des colonnes
        self.VERS_COL et self.ALIAS_COL. Cette fonction de rappel
        est connectée au signal self.fcChange.

        @param r la ligne du tableau où un changement a eu lieu
        """
        fieldname = self.fieldnames[r]
        c = self.tableWidget.cellWidget(r, self.VERS_COL)
        a = self.tableWidget.cellWidget(r, self.ALIAS_COL)
        if c:
            self.mw.updateConfig.emit(
                self.filename, fieldname, "foreign", c.currentText())
        if a:
            self.mw.updateConfig.emit(
                self.filename, fieldname, "alias", a.text())
        return

class TypeWidget(QComboBox):
    """
    Un widget pour choisir le type de champ. Les types possibles sont
    propres à la classe MaFenetre

    paramètres du constructeur:
    @param parent le widget parent
    @param row la ligne de tableau dans le widget parent
    @param fieldname le nom du champ pour la ligne courante ; il y a un
      traitement spécial quand la configuration est indéfinie et que
      le nom de champ commence par un préfixe spécial comme "Clé" ou "Id_"
    """

    prefixes_speciaux = {
        "cle" : "foreign",
        "id_"  : "int",
    }
    
    def __init__(self, parent, row, fieldname):
        QComboBox.__init__(self, parent)
        self.row = row
        self.mw = parent.mw
        self.parent = parent
        self.fieldname = fieldname
        self.addItems(self.mw.types.keys())
        parent.mw.registeredCombos[fieldname] = self
        atype = self.parent.getConf(fieldname, "type")
        if atype:
            self.setCurrentText(atype)
        else:
            # si le nom de champ commence par un préfixe spécial ...
            for prefixe, preference in self.prefixes_speciaux.items():
                if fieldname.lower().startswith(prefixe):
                    self.setCurrentText(preference)
        self.currentIndexChanged.connect(self.changed)
        return

    def changed(self):
        """
        fonction de rappel pour le changement de l'index courant
        """
        atype = self.currentText()
        if atype == "foreign":
            self.parent.foreignCells.emit(self.row, True, self.mw.modeles)
        else:
            self.parent.foreignCells.emit(self.row, False, [])
        self.mw.toDjango.emit()
        return
    

class UnicityWidget(QWidget,Ui_unicity):
    """
    Un widget avec des bouton radio pour clé primaire et unique.

    Paramètres du constructeur:
    @param parent le widget parent
    @param fieldname un nom de champ ; s'il commence par "Id_", le bouton
      de clé primaire sera coché
    """
    def __init__(self, parent, fieldname):
        QWidget.__init__(self, parent)
        Ui_unicity.__init__(self)
        self.setupUi(self)
        unicity = parent.getConf(fieldname, "unicity")
        if unicity == "primary_key":
            self.primButton.setChecked(True)
        elif unicity == "unique":
            self.uniqueButton.setChecked(True)
        else:
            # pas encore de config pour "unicity" ?
            # on regarde quand même si le nom de champ évoque une clé primaire
            if fieldname.lower().startswith("id_"):
                self.primButton.setChecked(True)
                parent.mw.updateConfig.emit(
                    parent.filename, fieldname, "unicity", "primary_key")
            else:
                self.radioButton.setChecked(True)
        # on connecte les trois boutons radio à la modification du code
        for button in (self.primButton, self.uniqueButton, self.radioButton):
            button.toggled.connect(lambda: parent.mw.toDjango.emit())
        return

    def __str__(self):
        if self.primButton.isChecked():
            return "primary_key"
        if self.uniqueButton.isChecked():
            return "unique"
        return "none"

    @property
    def isPrimaryKey(self):
        """
        @return Vrai si le bouton Clé primaire est coché
        """
        return self.primButton.isChecked()
    
    @property
    def isUnique(self):
        """
        @return Vrai si le bouton Unique est coché
        """
        return self.uniqueButton.isChecked()

    @property
    def djangoParam(self):
        """
        @return une mention pour un champ de modèle
        """
        if self.isPrimaryKey:
            return ", primary_key = True"
        elif self.isUnique:
            return ", unique = True"
        return ""
    
class ModelWidget(QComboBox):
    """
    Un widget pour choisir le modèle cible quand un champ est de type foreign

    paramètres du constructeur:
    @param parent le widget parent
    @param fieldname le nom du champ pour la ligne courante ; il y a un
      traitement spécial quand la configuration est indéfinie et que
      le nom de champ commence par un préfixe spécial comme "Clé" ou "Id"
    @param modeles une liste des modèles possibles
    """

    def __init__(self, parent, fieldname, modeles):
        QComboBox.__init__(self, parent)
        self.addItems(["Autre_Modele"] + list(modeles))
        return
    
def fn2modele(filename):
    """
    Déduit un nom de modèle du nom d'un fichier
    """
    return re.match(r".*/(.*)\.csv", filename).group(1)

def modele2fn(dir, modele):
    """
    Déduit un nom de fichier d'un nom de modèle
    """
    return os.path.join(dir, modele + ".csv")