+                ('MI36', models.CharField(max_length=255, null=True)),
71
+                ('MI39', models.CharField(max_length=255, null=True)),
72
+                ('MI24', models.CharField(max_length=255, null=True)),
73
+                ('Ind1', models.CharField(max_length=255, null=True)),
74
+                ('Ind2', models.CharField(max_length=255, null=True)),
75
+                ('Ind3', models.CharField(max_length=255, null=True)),
76
+                ('AGR1', models.CharField(max_length=255, null=True)),
77
+                ('AGR2', models.CharField(max_length=255, null=True)),
78
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
79
+            ],
80
+            options={
81
+                'db_table': 'bel_master_view',
82
+                'managed': False,
83
+            },
84
+        ),
85
+        migrations.CreateModel(
86
+            name='EMasterView',
87
+            fields=[
88
+                ('PRO0', models.CharField(max_length=255, primary_key=True, serialize=False)),
89
+                ('PRO1', models.CharField(max_length=255, null=True)),
90
+                ('PRO1C', models.CharField(max_length=255, null=True)),
91
+                ('PRO2', models.CharField(max_length=255, null=True)),
92
+                ('PRO5', models.CharField(max_length=255, null=True)),
93
+                ('PRO8', models.CharField(max_length=255, null=True)),
94
+                ('PRO9', models.CharField(max_length=255, null=True)),
95
+                ('PRO10', models.CharField(max_length=255, null=True)),
96
+                ('PRO11', models.CharField(max_length=255, null=True)),
97
+                ('PRO12', models.CharField(max_length=255, null=True)),
98
+                ('PRO13', models.CharField(max_length=255, null=True)),
99
+                ('PRO14', models.CharField(max_length=255, null=True)),
100
+                ('PRO15', models.CharField(max_length=255, null=True)),
101
+                ('PRO16', models.CharField(max_length=255, null=True)),
102
+                ('PRO17', models.CharField(max_length=255, null=True)),
103
+                ('PRO18', models.CharField(max_length=255, null=True)),
104
+                ('PRO21', models.CharField(max_length=255, null=True)),
105
+                ('PRO25', models.CharField(max_length=255, null=True)),
106
+                ('PRO27', models.CharField(max_length=255, null=True)),
107
+                ('P2', models.CharField(max_length=255, null=True)),
108
+                ('PRO6', models.CharField(max_length=255, null=True)),
109
+                ('SPEED', models.CharField(max_length=255, null=True)),
110
+                ('PRO4', models.CharField(max_length=255, null=True)),
111
+                ('MC11', models.CharField(max_length=255, null=True)),
112
+                ('MC12', models.CharField(max_length=255, null=True)),
113
+                ('MC14', models.CharField(max_length=255, null=True)),
114
+                ('MC15', models.CharField(max_length=255, null=True)),
115
+                ('MC16', models.CharField(max_length=255, null=True)),
116
+                ('MC19', models.CharField(max_length=255, null=True)),
117
+                ('MC20', models.CharField(max_length=255, null=True)),
118
+                ('MC21', models.CharField(max_length=255, null=True)),
119
+                ('MC22', models.CharField(max_length=255, null=True)),
120
+                ('MC23', models.CharField(max_length=255, null=True)),
121
+                ('MC24', models.CharField(max_length=255, null=True)),
122
+                ('MP45', models.CharField(max_length=255, null=True)),
123
+                ('MP49', models.CharField(max_length=255, null=True)),
124
+                ('MI13', models.CharField(max_length=255, null=True)),
125
+                ('TC', models.CharField(max_length=255, null=True)),
126
+                ('MI14', models.CharField(max_length=255, null=True)),
127
+                ('MI15', models.CharField(max_length=255, null=True)),
128
+                ('MI16', models.CharField(max_length=255, null=True)),
129
+                ('MI17', models.CharField(max_length=255, null=True)),
130
+                ('MI18', models.CharField(max_length=255, null=True)),
131
+                ('MI19', models.CharField(max_length=255, null=True)),
132
+                ('MI20', models.CharField(max_length=255, null=True)),
133
+                ('MI21', models.CharField(max_length=255, null=True)),
134
+                ('MI22', models.CharField(max_length=255, null=True)),
135
+                ('MI23', models.CharField(max_length=255, null=True)),
136
+                ('MI31', models.CharField(max_length=255, null=True)),
137
+                ('MI33', models.CharField(max_length=255, null=True)),
138
+                ('INSAGM', models.CharField(max_length=255, null=True)),
139
+                ('MARAGM', models.CharField(max_length=255, null=True)),
140
+                ('MI53', models.CharField(max_length=255, null=True)),
141
+                ('MI55', models.CharField(max_length=255, null=True)),
142
+                ('MI36', models.CharField(max_length=255, null=True)),
143
+                ('MI39', models.CharField(max_length=255, null=True)),
144
+                ('MI24', models.CharField(max_length=255, null=True)),
145
+                ('Ind1', models.CharField(max_length=255, null=True)),
146
+                ('Ind2', models.CharField(max_length=255, null=True)),
147
+                ('Ind3', models.CharField(max_length=255, null=True)),
148
+                ('AGR1', models.CharField(max_length=255, null=True)),
149
+                ('AGR2', models.CharField(max_length=255, null=True)),
150
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
151
+            ],
152
+            options={
153
+                'db_table': 'e_master_view',
154
+                'managed': False,
155
+            },
156
+        ),
157
+        migrations.CreateModel(
158
+            name='MgMasterView',
159
+            fields=[
160
+                ('PRO0', models.CharField(max_length=255, primary_key=True, serialize=False)),
161
+                ('PRO1', models.CharField(max_length=255, null=True)),
162
+                ('PRO1C', models.CharField(max_length=255, null=True)),
163
+                ('PRO2', models.CharField(max_length=255, null=True)),
164
+                ('PRO5', models.CharField(max_length=255, null=True)),
165
+                ('PRO8', models.CharField(max_length=255, null=True)),
166
+                ('PRO9', models.CharField(max_length=255, null=True)),
167
+                ('PRO10', models.CharField(max_length=255, null=True)),
168
+                ('PRO11', models.CharField(max_length=255, null=True)),
169
+                ('PRO12', models.CharField(max_length=255, null=True)),
170
+                ('PRO13', models.CharField(max_length=255, null=True)),
171
+                ('PRO14', models.CharField(max_length=255, null=True)),
172
+                ('PRO15', models.CharField(max_length=255, null=True)),
173
+                ('PRO16', models.CharField(max_length=255, null=True)),
174
+                ('PRO17', models.CharField(max_length=255, null=True)),
175
+                ('PRO18', models.CharField(max_length=255, null=True)),
176
+                ('PRO21', models.CharField(max_length=255, null=True)),
177
+                ('PRO25', models.CharField(max_length=255, null=True)),
178
+                ('PRO27', models.CharField(max_length=255, null=True)),
179
+                ('P2', models.CharField(max_length=255, null=True)),
180
+                ('SPEED', models.CharField(max_length=255, null=True)),
181
+                ('PRO4', models.CharField(max_length=255, null=True)),
182
+                ('MC11', models.CharField(max_length=255, null=True)),
183
+                ('MC12', models.CharField(max_length=255, null=True)),
184
+                ('MC14', models.CharField(max_length=255, null=True)),
185
+                ('MC15', models.CharField(max_length=255, null=True)),
186
+                ('MC16', models.CharField(max_length=255, null=True)),
187
+                ('MC19', models.CharField(max_length=255, null=True)),
188
+                ('MC20', models.CharField(max_length=255, null=True)),
189
+                ('MC21', models.CharField(max_length=255, null=True)),
190
+                ('MC22', models.CharField(max_length=255, null=True)),
191
+                ('MC23', models.CharField(max_length=255, null=True)),
192
+                ('MC24', models.CharField(max_length=255, null=True)),
193
+                ('MP45', models.CharField(max_length=255, null=True)),
194
+                ('MP49', models.CharField(max_length=255, null=True)),
195
+                ('MI13', models.CharField(max_length=255, null=True)),
196
+                ('TC', models.CharField(max_length=255, null=True)),
197
+                ('MI14', models.CharField(max_length=255, null=True)),
198
+                ('MI15', models.CharField(max_length=255, null=True)),
199
+                ('MI16', models.CharField(max_length=255, null=True)),
200
+                ('MI17', models.CharField(max_length=255, null=True)),
201
+                ('MI18', models.CharField(max_length=255, null=True)),
202
+                ('MI19', models.CharField(max_length=255, null=True)),
203
+                ('MI20', models.CharField(max_length=255, null=True)),
204
+                ('MI21', models.CharField(max_length=255, null=True)),
205
+                ('MI22', models.CharField(max_length=255, null=True)),
206
+                ('MI23', models.CharField(max_length=255, null=True)),
207
+                ('MI31', models.CharField(max_length=255, null=True)),
208
+                ('MI33', models.CharField(max_length=255, null=True)),
209
+                ('INSAGM', models.CharField(max_length=255, null=True)),
210
+                ('MARAGM', models.CharField(max_length=255, null=True)),
211
+                ('MI53', models.CharField(max_length=255, null=True)),
212
+                ('MI55', models.CharField(max_length=255, null=True)),
213
+                ('MI36', models.CharField(max_length=255, null=True)),
214
+                ('MI39', models.CharField(max_length=255, null=True)),
215
+                ('MI24', models.CharField(max_length=255, null=True)),
216
+                ('Ind1', models.CharField(max_length=255, null=True)),
217
+                ('Ind2', models.CharField(max_length=255, null=True)),
218
+                ('Ind3', models.CharField(max_length=255, null=True)),
219
+                ('AGR1', models.CharField(max_length=255, null=True)),
220
+                ('AGR2', models.CharField(max_length=255, null=True)),
221
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
222
+            ],
223
+            options={
224
+                'db_table': 'mg_master_view',
225
+                'managed': False,
226
+            },
227
+        ),
228
+        migrations.CreateModel(
229
+            name='VMasterView',
230
+            fields=[
231
+                ('PRO0', models.CharField(max_length=255, primary_key=True, serialize=False)),
232
+                ('PRO1', models.CharField(max_length=255, null=True)),
233
+                ('PRO1C', models.CharField(max_length=255, null=True)),
234
+                ('PRO2', models.CharField(max_length=255, null=True)),
235
+                ('PRO5', models.CharField(max_length=255, null=True)),
236
+                ('PRO8', models.CharField(max_length=255, null=True)),
237
+                ('PRO9', models.CharField(max_length=255, null=True)),
238
+                ('PRO10', models.CharField(max_length=255, null=True)),
239
+                ('PRO11', models.CharField(max_length=255, null=True)),
240
+                ('PRO12', models.CharField(max_length=255, null=True)),
241
+                ('PRO13', models.CharField(max_length=255, null=True)),
242
+                ('PRO14', models.CharField(max_length=255, null=True)),
243
+                ('PRO15', models.CharField(max_length=255, null=True)),
244
+                ('PRO16', models.CharField(max_length=255, null=True)),
245
+                ('PRO17', models.CharField(max_length=255, null=True)),
246
+                ('PRO18', models.CharField(max_length=255, null=True)),
247
+                ('PRO21', models.CharField(max_length=255, null=True)),
248
+                ('PRO25', models.CharField(max_length=255, null=True)),
249
+                ('PRO27', models.CharField(max_length=255, null=True)),
250
+                ('P2', models.CharField(max_length=255, null=True)),
251
+                ('PRO6', models.CharField(max_length=255, null=True)),
252
+                ('SPEED', models.CharField(max_length=255, null=True)),
253
+                ('PRO4', models.CharField(max_length=255, null=True)),
254
+                ('MC11', models.CharField(max_length=255, null=True)),
255
+                ('MC12', models.CharField(max_length=255, null=True)),
256
+                ('MC14', models.CharField(max_length=255, null=True)),
257
+                ('MC15', models.CharField(max_length=255, null=True)),
258
+                ('MC16', models.CharField(max_length=255, null=True)),
259
+                ('MC19', models.CharField(max_length=255, null=True)),
260
+                ('MC20', models.CharField(max_length=255, null=True)),
261
+                ('MC21', models.CharField(max_length=255, null=True)),
262
+                ('MC22', models.CharField(max_length=255, null=True)),
263
+                ('MC23', models.CharField(max_length=255, null=True)),
264
+                ('MC24', models.CharField(max_length=255, null=True)),
265
+                ('MP34', models.CharField(max_length=255, null=True)),
266
+                ('MP39', models.CharField(max_length=255, null=True)),
267
+                ('MI13', models.CharField(max_length=255, null=True)),
268
+                ('TC', models.CharField(max_length=255, null=True)),
269
+                ('MI14', models.CharField(max_length=255, null=True)),
270
+                ('MI15', models.CharField(max_length=255, null=True)),
271
+                ('MI16', models.CharField(max_length=255, null=True)),
272
+                ('MI17', models.CharField(max_length=255, null=True)),
273
+                ('MI18', models.CharField(max_length=255, null=True)),
274
+                ('MI19', models.CharField(max_length=255, null=True)),
275
+                ('MI20', models.CharField(max_length=255, null=True)),
276
+                ('MI21', models.CharField(max_length=255, null=True)),
277
+                ('MI22', models.CharField(max_length=255, null=True)),
278
+                ('MI23', models.CharField(max_length=255, null=True)),
279
+                ('MI31', models.CharField(max_length=255, null=True)),
280
+                ('MI33', models.CharField(max_length=255, null=True)),
281
+                ('INSAGM', models.CharField(max_length=255, null=True)),
282
+                ('MARAGM', models.CharField(max_length=255, null=True)),
283
+                ('MI53', models.CharField(max_length=255, null=True)),
284
+                ('MI55', models.CharField(max_length=255, null=True)),
285
+                ('MI36', models.CharField(max_length=255, null=True)),
286
+                ('MI39', models.CharField(max_length=255, null=True)),
287
+                ('MI24', models.CharField(max_length=255, null=True)),
288
+                ('Ind1', models.CharField(max_length=255, null=True)),
289
+                ('Ind2', models.CharField(max_length=255, null=True)),
290
+                ('Ind3', models.CharField(max_length=255, null=True)),
291
+                ('AGR1', models.CharField(max_length=255, null=True)),
292
+                ('AGR2', models.CharField(max_length=255, null=True)),
293
+                ('ob_Condition', models.CharField(max_length=255, null=True)),
294
+            ],
295
+            options={
296
+                'db_table': 'v_master_view',
297
+                'managed': False,
298
+            },
299
+        ),
300
+    ]

+ 2 - 2
app/legacy/templates/legacy/datacrud_list.html

@@ -30,7 +30,7 @@
30 30
     <thead>
31 31
         <tr class="bg-gray-100 text-left text-sm uppercase">
32 32
             {% for field in fields %}
33
-                <th class="border border-gray-200 px-4 py-2 text-left">{{ field.verbose_name }}</th>
33
+            <th class="border border-gray-200 px-4 py-2 text-left">{% firstof field.verbose_name field.name %} </th>
34 34
             {% endfor %}
35 35
             <th class="py-2 px-4 border-b">Actions</th>
36 36
         </tr>
@@ -47,7 +47,7 @@
47 47
                     {% elif field.name == 'file' and obj.file %}
48 48
                     <a href="{{ obj.file.url }}" target="_blank">View</a>
49 49
                     {% else %}
50
-                        {{ obj|attr:field.name }}
50
+                        {{ obj|attr:field.name | safe_floatformat:2 }}
51 51
                     {% endif %}
52 52
                 </td>
53 53
                 {% endfor %}

+ 10 - 0
app/legacy/templatetags/legacy_filters.py

@@ -1,5 +1,6 @@
1 1
 from django import template
2 2
 from django.template import Context
3
+from django.template.defaultfilters import floatformat
3 4
 
4 5
 
5 6
 register = template.Library()
@@ -27,3 +28,12 @@ def render_breadcrumbs(context, breadcrumbs):
27 28
     Context({
28 29
         'breadcrumbs': breadcrumbs
29 30
     }))
31
+
32
+@register.filter
33
+def safe_floatformat(value, decimal_places):
34
+    try:
35
+        # Attempt to convert the value to a float and format it
36
+        return floatformat(float(value), decimal_places)
37
+    except (ValueError, TypeError):
38
+        # If the value is not a number, return it as is
39
+        return value

+ 26 - 2
app/legacy/urls.py

@@ -1,6 +1,7 @@
1 1
 from django.urls import path
2 2
 from .views import DataListView, DataDetailView, DataCreateView, DataUpdateView, DataDeleteView,\
3
-DataMsCRUDView, TbFgPressInfoLotListCRUDView, LotSummaryCRUDView, VMasterViewCRUDView, MgMasterViewCRUDView, BelMasterViewCRUDView, EMasterViewCRUDView
3
+DataMsCRUDView, TbFgPressInfoLotListCRUDView, LotSummaryCRUDView, VMasterViewCRUDView, MgMasterViewCRUDView,\
4
+BelMasterViewCRUDView, EMasterViewCRUDView, DataRLCRUDView, DataWbCRUDView, LotSummaryRlCRUDView, LotSummaryWbCRUDView
4 5
 
5 6
 app_name = 'legacy'  # Namespace for this app
6 7
 
@@ -11,7 +12,10 @@ vm_crud = VMasterViewCRUDView()
11 12
 mg_crud = MgMasterViewCRUDView()
12 13
 bel_crud = BelMasterViewCRUDView()
13 14
 em_crud = EMasterViewCRUDView()
14
-
15
+datarl_crud = DataRLCRUDView()
16
+datawb_crud = DataWbCRUDView()
17
+lsrl_crud = LotSummaryRlCRUDView()
18
+lswb_crud = LotSummaryWbCRUDView()
15 19
 
16 20
 urlpatterns = [
17 21
     path('data/', DataListView.as_view(), name='data-list'),            # data/
@@ -24,6 +28,16 @@ urlpatterns = [
24 28
     path('datams/create/', datams_crud.get_create_view().as_view(), name='datams-create'),
25 29
     path('datams/<int:pk>/update/', datams_crud.get_update_view().as_view(), name='datams-update'),
26 30
     path('datams/<int:pk>/delete/', datams_crud.get_delete_view().as_view(), name='datams-delete'),
31
+
32
+    path('datarl/', datarl_crud.get_list_view().as_view(), name='datarl-list'),
33
+    path('datarl/create/', datarl_crud.get_create_view().as_view(), name='datarl-create'),
34
+    path('datarl/<int:pk>/update/', datarl_crud.get_update_view().as_view(), name='datarl-update'),
35
+    path('datarl/<int:pk>/delete/', datarl_crud.get_delete_view().as_view(), name='datarl-delete'),
36
+
37
+    path('datawb/', datawb_crud.get_list_view().as_view(), name='datawb-list'),
38
+    path('datawb/create/', datawb_crud.get_create_view().as_view(), name='datawb-create'),
39
+    path('datawb/<int:pk>/update/', datawb_crud.get_update_view().as_view(), name='datawb-update'),
40
+    path('datawb/<int:pk>/delete/', datawb_crud.get_delete_view().as_view(), name='datawb-delete'),
27 41
     
28 42
     path('fg/', fg_crud.get_list_view().as_view(), name='fg-list'),
29 43
     path('fg/create/', fg_crud.get_create_view().as_view(), name='fg-create'),
@@ -34,6 +48,7 @@ urlpatterns = [
34 48
     path('ls/create/', ls_crud.get_create_view().as_view(), name='ls-create'),
35 49
     path('ls/<int:pk>/update/', ls_crud.get_update_view().as_view(), name='ls-update'),
36 50
     path('ls/<int:pk>/delete/', ls_crud.get_delete_view().as_view(), name='ls-delete'),
51
+
37 52
     path('vm/', vm_crud.get_list_view().as_view(), name='vm-list'),
38 53
     path('vm/create/', vm_crud.get_create_view().as_view(), name='vm-create'),
39 54
     path('vm/<str:pk>/update/', vm_crud.get_update_view().as_view(), name='vm-update'),
@@ -55,4 +70,13 @@ urlpatterns = [
55 70
     path('em/<str:pk>/delete/', em_crud.get_delete_view().as_view(), name='em-delete'),
56 71
 
57 72
 
73
+    path('lsrl/', lsrl_crud.get_list_view().as_view(), name='lsrl-list'),
74
+    path('lsrl/create/', lsrl_crud.get_create_view().as_view(), name='lsrl-create'),
75
+    path('lsrl/<int:pk>/update/', lsrl_crud.get_update_view().as_view(), name='lsrl-update'),
76
+    path('lsrl/<int:pk>/delete/', lsrl_crud.get_delete_view().as_view(), name='lsrl-delete'),
77
+
78
+    path('lswb/', lswb_crud.get_list_view().as_view(), name='lswb-list'),
79
+    path('lswb/create/', lswb_crud.get_create_view().as_view(), name='lswb-create'),
80
+    path('lswb/<int:pk>/update/', lswb_crud.get_update_view().as_view(), name='lswb-update'),
81
+    path('lswb/<int:pk>/delete/', lswb_crud.get_delete_view().as_view(), name='lswb-delete'),
58 82
 ]

+ 75 - 3
app/legacy/views.py

@@ -11,8 +11,9 @@ from django.views.generic import (
11 11
     DeleteView,
12 12
 )
13 13
 from django.core.paginator import Paginator
14
-from .models import Data, DataMs, TbFgPressinfoLotlist, LotSummary
15
-from .filters import DataFilter, DataMsFilter, TbFgPressFilter, LotSummaryFilter
14
+from .models import Data, DataMs, TbFgPressinfoLotlist, LotSummary, DataRl, DataWb, LotSummaryRl, LotSummaryWb
15
+from .filters import DataFilter, DataMsFilter, TbFgPressFilter, LotSummaryFilter, \
16
+        DataRlFilter, DataWbFilter, LotSummaryRlFilter, LotSummaryWbFilter
16 17
 from django.urls import reverse
17 18
 from django.contrib import messages
18 19
 from pprint import pprint
@@ -158,7 +159,7 @@ class LotSummaryCRUDView(ConfigurableCRUDView):
158 159
     detail_template_name = 'legacy/datacrud_detail.html'
159 160
     form_template_name = 'legacy/datacrud_form.html'
160 161
     confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
161
-    filterset_class = DataMsFilter
162
+    filterset_class = LotSummaryFilter
162 163
 
163 164
     page_title = "Lot Summary"
164 165
 
@@ -301,3 +302,74 @@ class EMasterViewCRUDView(ConfigurableCRUDView):
301 302
     # Default ordering
302 303
     # ordering = ["-id", "PRO2"]
303 304
     
305
+class DataRLCRUDView(ConfigurableCRUDView):
306
+    model = DataRl
307
+    list_template_name = 'legacy/datacrud_list.html'
308
+    detail_template_name = 'legacy/datacrud_detail.html'
309
+    form_template_name = 'legacy/datacrud_form.html'
310
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
311
+    filterset_class = DataRlFilter
312
+
313
+    page_title = "Data RL"
314
+
315
+    # URL name mappings
316
+    list_url_name = 'legacy:datarl-list'
317
+    create_url_name = 'legacy:datarl-create'
318
+    update_url_name = 'legacy:datarl-update'
319
+    delete_url_name = 'legacy:datarl-delete'
320
+    # excludes = ["splitdata"]
321
+    config_field_orders = ["id", "lot_no", "code"]  # Display these fields first
322
+
323
+class DataWbCRUDView(ConfigurableCRUDView):
324
+    model = DataWb
325
+    list_template_name = 'legacy/datacrud_list.html'
326
+    detail_template_name = 'legacy/datacrud_detail.html'
327
+    form_template_name = 'legacy/datacrud_form.html'
328
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
329
+    filterset_class = DataWbFilter
330
+
331
+    page_title = "Data WB"
332
+
333
+    # URL name mappings
334
+    list_url_name = 'legacy:datawb-list'
335
+    create_url_name = 'legacy:datawb-create'
336
+    update_url_name = 'legacy:datawb-update'
337
+    delete_url_name = 'legacy:datawb-delete'
338
+    # excludes = ["splitdata"]
339
+    config_field_orders = ["id", "lot_no", "code"]  # Display these fields first
340
+
341
+class LotSummaryRlCRUDView(ConfigurableCRUDView):
342
+    model = LotSummaryRl
343
+    list_template_name = 'legacy/datacrud_list.html'
344
+    detail_template_name = 'legacy/datacrud_detail.html'
345
+    form_template_name = 'legacy/datacrud_form.html'
346
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
347
+    filterset_class = LotSummaryRlFilter
348
+
349
+    page_title = "Lot Summary RL"
350
+
351
+    # URL name mappings
352
+    list_url_name = 'legacy:lsrl-list'
353
+    create_url_name = 'legacy:lsrl-create'
354
+    update_url_name = 'legacy:lsrl-update'
355
+    delete_url_name = 'legacy:lsrl-delete'
356
+    # excludes = ["splitdata"]
357
+    config_field_orders = ["id", "lot_no", "code"]  # Display these fields first
358
+
359
+class LotSummaryWbCRUDView(ConfigurableCRUDView):
360
+    model = LotSummaryWb
361
+    list_template_name = 'legacy/datacrud_list.html'
362
+    detail_template_name = 'legacy/datacrud_detail.html'
363
+    form_template_name = 'legacy/datacrud_form.html'
364
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
365
+    filterset_class = LotSummaryWbFilter
366
+
367
+    page_title = "Lot Summary WB"
368
+
369
+    # URL name mappings
370
+    list_url_name = 'legacy:lswb-list'
371
+    create_url_name = 'legacy:lswb-create'
372
+    update_url_name = 'legacy:lswb-update'
373
+    delete_url_name = 'legacy:lswb-delete'
374
+    # excludes = ["splitdata"]
375
+    config_field_orders = ["id", "lot_no", "code"]  # Display these fields first

+ 1 - 0
app/package.json

@@ -1,6 +1,7 @@
1 1
 {
2 2
   "dependencies": {
3 3
     "alpinejs": "^3.14.7",
4
+    "axios": "^1.7.9",
4 5
     "font-awesome": "^4.7.0",
5 6
     "heroicons": "^2.2.0",
6 7
     "tailwindcss": "^3.4.17",

BIN
app/report/checkbox_checked.jpg


BIN
app/report/checkbox_unchecked.jpg


BIN
app/report/coi_templates.xlsx


+ 280 - 0
app/report/gen_report.py

@@ -0,0 +1,280 @@
1
+
2
+from openpyxl import load_workbook, Workbook
3
+from datetime import datetime
4
+from openpyxl.drawing.image import Image
5
+import re
6
+
7
+from openpyxl.drawing.spreadsheet_drawing import AbsoluteAnchor
8
+from openpyxl.drawing.xdr import XDRPoint2D, XDRPositiveSize2D
9
+from openpyxl.utils.units import pixels_to_EMU
10
+from openpyxl.utils import get_column_letter, column_index_from_string
11
+from django.db import models
12
+import os
13
+from django.db.models.fields.files import ImageFieldFile
14
+from pprint import pprint
15
+from PIL import Image as PILImage
16
+from openpyxl.drawing.xdr import XDRPoint2D, XDRPositiveSize2D
17
+from openpyxl.utils.units import pixels_to_EMU
18
+from openpyxl.drawing.spreadsheet_drawing import AnchorMarker, TwoCellAnchor
19
+
20
+
21
+def set_image_with_offset_old(sheet, img, cell_coordinate, offset_x=0, offset_y=0):
22
+    """
23
+    Add an image to the sheet with an offset relative to the top-left corner of a cell.
24
+
25
+    :param sheet: The worksheet
26
+    :param img: The openpyxl Image object
27
+    :param cell_coordinate: Cell to place the image (e.g., "B2")
28
+    :param offset_x: Horizontal offset in pixels
29
+    :param offset_y: Vertical offset in pixels
30
+    """
31
+    col_letter = ''.join(filter(str.isalpha, cell_coordinate))  # Extract column letter
32
+    row_number = int(''.join(filter(str.isdigit, cell_coordinate)))  # Extract row number
33
+
34
+    # Get column width and row height in pixels
35
+    col_width = sheet.column_dimensions[get_column_letter(column_index_from_string(col_letter))].width or 10
36
+    row_height = sheet.row_dimensions[row_number].height or 15
37
+
38
+    # Approximate conversion of Excel units to pixels
39
+    col_pixels = col_width * 7.5  # Excel's ~7.5 pixels per width unit
40
+    row_pixels = row_height * 0.75  # Approximation for row height in pixels
41
+
42
+    # Calculate absolute positions based on offsets
43
+    anchor_x = col_pixels + offset_x
44
+    anchor_y = row_pixels + offset_y
45
+
46
+    # Set the anchor for the image
47
+    img.anchor = cell_coordinate
48
+    img.anchor.dx = int(anchor_x * 9525)  # Convert to EMUs (Excel Measurement Units)
49
+    img.anchor.dy = int(anchor_y * 9525)  # Convert to EMUs (Excel Measurement Units)
50
+
51
+    sheet.add_image(img)
52
+
53
+
54
+def set_image_with_offset(sheet, img, cell_coordinate, offset_x=0, offset_y=0):
55
+    """
56
+    Add an image to the sheet with an offset relative to the top-left corner of a cell.
57
+
58
+    :param sheet: The worksheet
59
+    :param img: The openpyxl Image object
60
+    :param cell_coordinate: Cell to place the image (e.g., "B2")
61
+    :param offset_x: Horizontal offset in pixels
62
+    :param offset_y: Vertical offset in pixels
63
+    """
64
+    # Extract the column and row from the cell coordinate
65
+    col_letter = ''.join(filter(str.isalpha, cell_coordinate))  # Extract column letter
66
+    row_number = int(''.join(filter(str.isdigit, cell_coordinate)))  # Extract row number
67
+
68
+    # Get the zero-based indices for the cell
69
+    col_idx = column_index_from_string(col_letter) - 1
70
+    row_idx = row_number - 1
71
+
72
+    # Approximate column width and row height to pixels
73
+    col_width = sheet.column_dimensions[col_letter].width or 10  # Default column width
74
+    row_height = sheet.row_dimensions[row_number].height or 15  # Default row height
75
+
76
+    # Convert column width and row height to pixels
77
+    col_pixels = col_width * 7.5  # Approximation: ~7.5 pixels per width unit
78
+    row_pixels = row_height * 0.75  # Approximation: ~0.75 pixels per height unit
79
+
80
+    # Calculate the position in pixels for the top-left corner of the cell
81
+    cell_x = col_idx * col_pixels
82
+    cell_y = row_idx * row_pixels
83
+
84
+    # Apply the offsets
85
+    final_x = cell_x + offset_x
86
+    final_y = cell_y + offset_y
87
+
88
+    # Convert to EMUs
89
+    pos = XDRPoint2D(pixels_to_EMU(final_x), pixels_to_EMU(final_y))
90
+    size = XDRPositiveSize2D(pixels_to_EMU(img.width), pixels_to_EMU(img.height))
91
+
92
+    # Set the image's anchor with the position and size
93
+    img.anchor = AbsoluteAnchor(pos=pos, ext=size)
94
+
95
+    # Add the image to the worksheet
96
+    sheet.add_image(img)
97
+
98
+
99
+
100
+def center_image_in_cell(sheet, img, cell_coordinate):
101
+    """
102
+    Center an image inside a specified cell.
103
+
104
+    :param sheet: The worksheet
105
+    :param img: The openpyxl Image object
106
+    :param cell_coordinate: The cell to center the image in (e.g., "C3")
107
+    """
108
+    # Extract column and row from the cell coordinate
109
+    col_letter = ''.join(filter(str.isalpha, cell_coordinate))  # Extract column letter
110
+    row_number = int(''.join(filter(str.isdigit, cell_coordinate)))  # Extract row number
111
+    col_idx = column_index_from_string(col_letter) - 1  # Convert to zero-based column index
112
+
113
+    # Get cell dimensions
114
+    col_width = sheet.column_dimensions[col_letter].width or 10  # Default width if not set
115
+    row_height = sheet.row_dimensions[row_number].height or 15  # Default height if not set
116
+
117
+    # Convert dimensions to pixels (approximation)
118
+    col_pixels = col_width * 7.5  # 1 Excel column width unit ≈ 7.5 pixels
119
+    row_pixels = row_height * 0.75  # 1 Excel row height unit ≈ 0.75 pixels
120
+
121
+    # Get image dimensions
122
+    img_width, img_height = img.width, img.height
123
+
124
+    # Calculate offsets to center the image
125
+    offset_x = int((col_pixels - img_width) / 2 * pixels_to_EMU(1))  # Center horizontally
126
+    offset_y = int((row_pixels - img_height) / 2 * pixels_to_EMU(1))  # Center vertically
127
+
128
+    # Define the anchor for the image
129
+    _from = AnchorMarker(col=col_idx, row=row_number - 1, colOff=offset_x, rowOff=offset_y)
130
+    to = AnchorMarker(col=col_idx + 1, row=row_number, colOff=-offset_x, rowOff=-offset_y)
131
+
132
+    # Use TwoCellAnchor for positioning
133
+    img.anchor = TwoCellAnchor(editAs="oneCell", _from=_from, to=to)
134
+
135
+    # Add the image to the sheet
136
+    sheet.add_image(img)
137
+
138
+
139
+def gen_xlsx(template_file, selected_sheets, prefix_filename, data):
140
+    """
141
+    Generate an Excel file from a template, fill placeholders, and include only selected sheets.
142
+
143
+    Args:
144
+        template_file (str): Path to the Excel template file.
145
+        selected_sheets (list): List of sheet names to include in the output file.
146
+        prefix_filename (str): Prefix for the output filename.
147
+        data (dict): Data dictionary with sheet-specific keys and fallback keys.
148
+
149
+    Returns:
150
+        str: Path of the generated Excel file.
151
+    """
152
+    checked_image_path = "/app/report/checkbox_checked.jpg"  # Path to the checked checkbox image
153
+    unchecked_image_path = "/app/report/checkbox_unchecked.jpg"  # Path to the unchecked checkbox image
154
+
155
+    # Load the template workbook
156
+    workbook = load_workbook(template_file)
157
+
158
+    # Remove sheets not in selected_sheets
159
+    for sheet_name in workbook.sheetnames:
160
+        if sheet_name not in selected_sheets:
161
+            del workbook[sheet_name]
162
+
163
+    # Process the selected sheets
164
+    for sheet_name in selected_sheets:
165
+        if sheet_name not in workbook.sheetnames:
166
+            raise ValueError(f"Sheet '{sheet_name}' not found in the template.")
167
+
168
+        sheet = workbook[sheet_name]
169
+
170
+        # Replace placeholders with actual values
171
+        # Handle hiding rows based on patterns in data
172
+         
173
+        for row in sheet.iter_rows():
174
+            for cell in row:
175
+                if cell.value and isinstance(cell.value, str) and cell.value.startswith("<") and cell.value.endswith(">"):
176
+                    placeholder = cell.value.strip("<>")
177
+                    
178
+                    # Determine value priority: `sheet_name.key` > `key`
179
+                    value = None
180
+                    sheet_specific_key = f"{sheet_name}.{placeholder}"
181
+                    if sheet_specific_key in data:
182
+                        value = data[sheet_specific_key]
183
+                    elif placeholder in data:
184
+                        value = data[placeholder]
185
+                        
186
+                    if value is not None:
187
+                        if isinstance(value, ImageFieldFile):
188
+                            pprint("ImageField")
189
+                            image_path = value.path
190
+                            if os.path.exists(image_path):
191
+                                # img = Image(image_path)
192
+                                # img.height = 40  # Adjust size as needed
193
+                                pil_img = PILImage.open(image_path)
194
+                                original_width, original_height = pil_img.size
195
+
196
+                                # Desired height (e.g., 40), calculate the new width to maintain aspect ratio
197
+                                desired_height = 40
198
+                                aspect_ratio = original_width / original_height
199
+                                new_width = int(desired_height * aspect_ratio)
200
+
201
+                                # Resize the image using Pillow (optional, for saving memory during export)
202
+                                resized_img = pil_img.resize((new_width, desired_height), PILImage.Resampling.LANCZOS)
203
+                                resized_img.save(image_path)  # Save the resized image back to the same path
204
+
205
+                                # Insert the resized image into the Excel sheet
206
+                                img = Image(image_path)
207
+                                img.width, img.height = new_width, desired_height  # Set the dimensions
208
+                                # sheet.add_image(img, cell.coordinate)
209
+                                center_image_in_cell(sheet, img, cell.coordinate, )
210
+                                cell.value = None  # Clear placeholder
211
+
212
+                        elif value is True:
213
+                            img = Image(checked_image_path)
214
+                            img.width = img.height = 10
215
+                            print(f"{cell.coordinate}")
216
+                            # sheet.add_image(img, cell.coordinate)
217
+                            # set_image_with_offset(sheet, img, cell.coordinate, offset_x=100)
218
+                            center_image_in_cell(sheet,img, cell.coordinate, )
219
+                            cell.value = None  # Remove the placeholder text
220
+                        elif value is False:
221
+                            img = Image(unchecked_image_path)
222
+                            img.width = img.height = 10
223
+                            # sheet.add_image(img, cell.coordinate)
224
+                            # set_image_with_offset(sheet, img, cell.coordinate, offset_x=100)
225
+                            center_image_in_cell(sheet, img, cell.coordinate, )
226
+                            cell.value = None  # Remove the placeholder text
227
+                        else:
228
+                            # Insert the text value directly
229
+                            cell.value = value
230
+
231
+        for key, value in data.items():
232
+            if isinstance(value, str) and re.match(r"^\d+\[\d+:\d+\]$", value):
233
+                # Parse the prefix and row range
234
+                prefix, row_range = value.split("[")
235
+                row_start, row_end = map(int, row_range[:-1].split(":"))
236
+                
237
+                # Hide rows if the prefix matches the condition
238
+                if prefix == "0":  # Adjust the condition as needed
239
+                    sheet.row_dimensions.group(row_start, row_end, hidden=True)
240
+        
241
+
242
+    # Generate the output filename with a timestamp
243
+    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
244
+    output_path = f"{prefix_filename}_{timestamp}.xlsx"
245
+    workbook.save(output_path)
246
+
247
+    return output_path
248
+
249
+if __name__ == "__main__":
250
+    # Example usage
251
+    data = {
252
+        "customer": "Tum Coder",
253
+        "inspect_date": "2025-01-15",
254
+        "lot_no": "12345",
255
+        "staff_name":  "Tum 8888",
256
+        "man_name":  "Tum 999",
257
+        "size": "Large",
258
+        "pcs": "10 pcs",
259
+        "spec": "Spec-A",
260
+        "hardness.d1_act": "10",
261
+        "hardness.d2_act": "0[24:28]",  # Hide rows 24 to 28 if the prefix is "0"
262
+        "hardness.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
263
+        "hardness.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
264
+        "dimension_app.d1_act": "33",
265
+        "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
266
+        "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
267
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
268
+    }
269
+
270
+
271
+
272
+
273
+    output_file = gen_xlsx(
274
+        template_file="./hardness.xlsx",
275
+        selected_sheets=["hardness", "dimension_app"],  # Replace with your actual sheet names
276
+        prefix_filename="./output/output",
277
+        data=data
278
+    )
279
+
280
+    print(f"Generated file: {output_file}")

+ 98 - 45
app/report/templates/report/coi.html

@@ -3,66 +3,63 @@
3 3
 {% block title %}Report Dashboard{% endblock %}
4 4
 
5 5
 {% block content %}
6
-<div class="container mx-auto px-4 py-8">
6
+<div class="container mx-auto px-4 py-8" x-data="COIReport">
7 7
 
8 8
   <h1 class="text-2xl font-bold text-gray-800">Export Center</h1>
9 9
   <form method='post'>
10 10
     {% csrf_token %}
11
-    <div class="flex items-center justify-between mb-4">
12
-      <h1 class="text-lg font-bold text-gray-800">TKX Certificate Issue</h1>
13
-      <button class="bg-gray-300 text-gray-700 px-4 py-2 rounded hover:bg-gray-400">
14
-        รายละเอียด CheckedBy|ApproveBy
15
-      </button>
16
-    </div>
17 11
     <div class="flex items-center gap-2 mb-4">
18 12
       <label for="lot-number" class="text-gray-700 font-medium">Lot No. :</label>
19
-      <input id="lot-number" type="text" class="border border-gray-300 rounded px-4 py-2 focus:outline-blue-500" placeholder="Enter Lot No." name='lot_no' required>
13
+      <input id="lot-number" type="text" class="border border-gray-300 rounded px-4 py-2 focus:outline-blue-500" placeholder="Enter Lot No." name='lot_no' required x-model="lot_no">
20 14
       <button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" type="submit" name="search_lot">
21 15
         🔍
22 16
       </button>
23 17
     </div>
24
-    <div class="flex items-center gap-4 mb-4">
25 18
       <button class="bg-blue-100 text-blue-700 px-4 py-2 rounded hover:bg-blue-200">
26 19
         Option for Export :
27 20
       </button>
28
-      <div class="flex items-center space-x-2">
29
-        <label class="flex items-center space-x-1">
30
-          <input type="checkbox" class="rounded" name='exports' value='size_dimension'>
31
-          <span>Size Dimension</span>
32
-        </label>
33
-        <label class="flex items-center space-x-1">
34
-          <input type="checkbox" class="rounded" name='exports' value='grade'>
35
-          <span>Grade</span>
36
-        </label>
21
+        <div class="grid grid-cols-4 gap-4">
22
+        {% for key,value in SHEET_NAMES.items %}
37 23
         <label class="flex items-center space-x-1">
38
-          <input type="checkbox" class="rounded" name='exports' value='wb'>
39
-          <span>WB</span>
24
+          <input type="checkbox" class="rounded" name='exports' value='{{ key }}' x-model='exports'>
25
+          <span>{{ value }}</span>
40 26
         </label>
41
-        <label class="flex items-center space-x-1">
42
-          <input type="checkbox" class="rounded" name='exports' value='fg_weight'>
43
-          <span>FG Weight</span>
44
-        </label>
45
-        <label class="flex items-center space-x-1">
46
-          <input type="checkbox" class="rounded" name='exports' value='centering'>
47
-          <span>Centering</span>
48
-        </label>
49
-        <label class="flex items-center space-x-1">
50
-          <input type="checkbox" class="rounded" name='exports' value='ring_test'>
51
-          <span>Ring Test</span>
52
-        </label>
53
-        <label class="flex items-center space-x-1">
54
-          <input type="checkbox" class="rounded" name='exports' value='rotate'>
55
-          <span>Rotate</span>
56
-        </label>
57
-        <label class="flex items-center space-x-1">
58
-          <input type="checkbox" class="rounded" name='exports' value='final_judge'>
59
-          <span>Final Judge</span>
60
-        </label>
61
-      </div>
62
-      <button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" type='submit' name='export'>
63
-        Export Excel
64
-      </button>
65
-    </div>
27
+        {% endfor %}
28
+        </div>
29
+        <div class="grid grid-cols-2 gap-4">
30
+          <div class="my-4">
31
+            <label for="qa1" class="block mb-2 text-sm font-medium text-gray-900">Select QA.1</label>
32
+            <select id="qa1" name="qa1" class="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" x-model='qa1'>
33
+              <option value="" disabled selected>Choose a user</option>
34
+              {% for user in users %}
35
+              <option value="{{ user.id }}">{{ user.profile }}</option>
36
+              {% endfor %}
37
+            </select>
38
+          </div>
39
+          <div class="my-4">
40
+            <label for="qa2" class="block mb-2 text-sm font-medium text-gray-900">Select QA.2</label>
41
+            <select id="qa2" name="qa2" class="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" x-model='qa2'>
42
+              <option value="" disabled selected>Choose a user</option>
43
+              {% for user in users %}
44
+              <option value="{{ user.id }}">{{ user.profile }}</option>
45
+              {% endfor %}
46
+            </select>
47
+          </div>
48
+        </div>
49
+        <div class="flex justify-end my-3 space-x-4">
50
+          <div>
51
+            <button  type='button' class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" 
52
+                                   @click="exportCOI">
53
+              Export Excel
54
+            </button>
55
+          </div>
56
+          <!-- TODO: add download here -->
57
+          <div class=" text-center" x-show="downloadUrl">
58
+            <a :href="downloadUrl"  class="block px-4 py-2 bg-green-600 text-white  rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2" download>
59
+              Download Report
60
+            </a>
61
+          </div>
62
+        </div>
66 63
   </form>
67 64
   {{ result }}
68 65
   
@@ -130,3 +127,59 @@
130 127
 <div class="border-t border-gray-300 my-4"></div>
131 128
 {% include "report/_cen.html" %}
132 129
 {% endblock %}
130
+{% block footer_script %}
131
+<script type="text/javascript">
132
+  function COIReport() {
133
+    return {
134
+      lot_no: '', // Bind this to the input value
135
+      exports: [],
136
+      qa1: null,
137
+      qa2: null,
138
+      gen_report_url: '{% url "report:gen_report" %}', 
139
+      downloadUrl: null, // Stores the download link after export success
140
+      init() {
141
+        //alert("COI Report");
142
+        this.qa1 = '';
143
+        this.qa2 = '';
144
+      },
145
+      async exportCOI() {
146
+              if (!this.lot_no) {
147
+                  alert("Please enter a Lot No.");
148
+                  return;
149
+              }
150
+
151
+              try {
152
+                  // Make POST request using Axios
153
+                  const response = await axios.post(this.gen_report_url, {
154
+                      lot_no: this.lot_no,
155
+                      exports: this.exports,
156
+                      qa1: this.qa1,
157
+                      qa2: this.qa2
158
+                  });
159
+
160
+                  if (response.status === 200) {
161
+                      const result = response.data;
162
+                      alert(`Report generated successfully: ${result.file_url}`);
163
+                      console.log('File URL:', result.file_url);
164
+                      this.downloadUrl = result.file_url; // Set the download URL
165
+                  }
166
+              } catch (error) {
167
+                  if (error.response) {
168
+                      // Server responded with a status other than 2xx
169
+                      alert(`Error: ${error.response.data.message || 'Failed to generate report'}`);
170
+                      console.error('Error response:', error.response);
171
+                  } else if (error.request) {
172
+                      // Request was made but no response received
173
+                      alert('No response from server.');
174
+                      console.error('Request error:', error.request);
175
+                  } else {
176
+                      // Something else caused the error
177
+                      alert('An unexpected error occurred.');
178
+                      console.error('Unexpected error:', error.message);
179
+                  }
180
+              }
181
+          },
182
+    };
183
+  }
184
+</script>
185
+{% endblock %}

+ 2 - 0
app/report/urls.py

@@ -14,6 +14,8 @@ urlpatterns = [
14 14
     path('report/<int:pk>/update/', report_crud.get_update_view().as_view(), name='report-update'),
15 15
     path('report/<int:pk>/delete/', report_crud.get_delete_view().as_view(), name='report-delete'),
16 16
     path('coi/', views.coi_view, name='coi-view'),
17
+    path('report/generate/', views.gen_report_view, name='gen_report'),
18
+
17 19
     # path('create/', views.create_report, name='create'),  # Create a new report
18 20
     # path('<int:pk>/', views.detail_report, name='detail'),  # View details of a specific report
19 21
     # path('<int:pk>/update/', views.update_report, name='update'),  # Update a specific report

+ 223 - 3
app/report/views.py

@@ -8,6 +8,16 @@ from .filters import ReportFilter
8 8
 from .forms import ExportOptionsForm
9 9
 from pprint import pprint
10 10
 
11
+from .gen_report import gen_xlsx
12
+from django.core.files.base import File
13
+from pathlib import Path
14
+from django.views.decorators.csrf import csrf_exempt
15
+from django.http import JsonResponse, HttpResponseBadRequest
16
+import json
17
+from django.contrib.auth.decorators import login_required
18
+from django.contrib.auth.models import User
19
+from legacy.models import Data
20
+
11 21
 
12 22
 def index(request):
13 23
     reports = Report.objects.all()
@@ -58,9 +68,183 @@ class ReportCRUDView(ConfigurableCRUDView):
58 68
     # config_edit_fields = ["lot_no", "code"]
59 69
     ordering = ["-created_at", "-id",]
60 70
 
71
+def convert_sheet_data(sheet_data):
72
+    """
73
+    Convert sheet_data to the required form with prefixed keys.
74
+    
75
+    :param sheet_data: Dictionary with sheet names as keys and their data as values.
76
+    :return: Dictionary in the required key-value format.
77
+    """
78
+    converted_data = {}
79
+
80
+    for sheet_name, data in sheet_data.items():
81
+        for key, value in data.items():
82
+            # Prefix each key with the sheet name
83
+            converted_key = f"{sheet_name}.{key}"
84
+            converted_data[converted_key] = value
85
+
86
+    return converted_data
87
+
88
+def generate_hardness_out_values(lot_no):
89
+    """
90
+    Generate a dictionary of placeholder values for a given lot_no.
91
+
92
+    :param lot_no: The lot number to query data for.
93
+    :return: A dictionary with placeholders (e.g., v1_1, v1_2, ...) as keys and their respective values.
94
+    """
95
+    # Query the Data model for records matching the given lot_no
96
+    records = Data.objects.filter(lot_no=lot_no).order_by('row_no')
97
+    print(f"records {lot_no} = {records.values()}")
98
+    # Initialize an empty dictionary to store placeholder values
99
+    placeholders = {}
100
+
101
+    # Iterate over the records to populate placeholder values
102
+    for record_idx, record in enumerate(records, start=1):
103
+        placeholders[f'v{record_idx}_1'] = record.p1  # Checkpoint 1 value
104
+        placeholders[f'v{record_idx}_2'] = record.p2  # Checkpoint 2 value
105
+        placeholders[f'v{record_idx}_3'] = record.p3  # Checkpoint 3 value
106
+        placeholders[f'v{record_idx}_4'] = record.avg  # Average value
107
+        placeholders[f'v{record_idx}_5'] = record.rgrade  # Judgment value
108
+
109
+    return placeholders
110
+
111
+def merge_sheet_data_with_data(sheet_data, data):
112
+    """
113
+    Merge `sheet_data` with `data`.
114
+
115
+    :param sheet_data: Dictionary containing the sheet-specific data.
116
+    :param data: Dictionary containing general data.
117
+    :return: A merged dictionary combining both `sheet_data` and `data`.
118
+    """
119
+    # Merge dictionaries using unpacking
120
+    merged_data = {**data, **sheet_data}
121
+
122
+    return merged_data
123
+
124
+def create_coi_file(lot_no, sheets, user, md):
125
+    pprint("---- create_coi_file ---")
126
+    pprint(md)
127
+    qa1 = User.objects.get(pk=md['qa1'])
128
+    qa2 = User.objects.get(pk=md['qa2'])
129
+
130
+    pprint(qa1)
131
+    pprint(qa2)
132
+    
133
+    sheet_data = {}
134
+    for sheet_name in sheets:
135
+        if sheet_name == 'hardness_out':
136
+            sheet_data[sheet_name] = generate_hardness_out_values(lot_no)
137
+    converted_data = convert_sheet_data(sheet_data)
138
+
139
+    print(f"sheet_data \n {sheet_data}") 
140
+    print(f"converted_data \n {converted_data}")
141
+
142
+    data = {
143
+        "customer": "Tum Coder",
144
+        "inspect_date": "2025-01-15",
145
+        "lot_no": "12345",
146
+        "staff_name":  "Tum 8888",
147
+        "man_name":  "Tum 999",
148
+        "size": "Large",
149
+        "pcs": "10 pcs",
150
+        "spec": "Spec-A",
151
+        "hardness_out.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
152
+        "hardness_out.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
153
+        "acc": True,  # Hide rows 24 to 28 if the prefix is "0"
154
+        "spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
155
+        # "hardness_out.qa1": f"{qa1.first_name} {qa1.last_name}",
156
+        # "hardness_out.qa2": f"{qa2.first_name} {qa2.last_name}",
157
+        "qa1": f"{qa1.first_name} {qa1.last_name}",
158
+        "qa2": f"{qa2.first_name} {qa2.last_name}",
159
+        "dimension_app.d1_act": "33",
160
+        "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
161
+        "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
162
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
163
+        "sign1": qa1.profile.signed_picture,
164
+        "sign2": qa2.profile.signed_picture,
165
+        "pos1": qa1.profile.get_position_display(),
166
+        "pos2": qa2.profile.get_position_display()
167
+    }
168
+    merged_data = merge_sheet_data_with_data(converted_data, data)
169
+    pprint(f"---- merged_data ---")
170
+    pprint(merged_data)
171
+
172
+    output_file = gen_xlsx(
173
+        template_file="/app/report/coi_templates.xlsx",
174
+        selected_sheets=sheets,  # Replace with your actual sheet names
175
+        prefix_filename="/app/media/coi",
176
+        data=merged_data
177
+    )
178
+    report = Report.objects.create(
179
+        name=lot_no,
180
+        created_by=user,
181
+        file=None  # Leave this as None or assign a file if required
182
+    )
183
+    output_file_path = Path(output_file)  # Convert to a Path object for convenience
184
+    with open(output_file_path, "rb") as f:
185
+        report.file.save(output_file_path.name, File(f), save=True)
186
+
187
+    pprint(f"outputfile = {output_file}")
188
+    return report
189
+
190
+SHEET_NAMES = {
191
+    'hardness_out': 'Hardness Out',
192
+    'hardness_out_in': 'Hardness Out/In', 
193
+    'hardness_both_size': 'Hardness Both Size',
194
+    'dimension': 'Dimension',
195
+    'dimension_app': 'Dimension Appearance',
196
+    'dimension_bal_weight': 'Dimension Balance/Weight',
197
+    'dim_bal_app_hard': 'Dimension Balance/Appearance/Hardness',
198
+    'dim_bal_app_rot_hard': 'Dimension Balance/Appearance/Rotation/Hardness',
199
+    'thickness_8_point': 'Thickness 8 Points',
200
+    'centering': 'Centering',
201
+}
61 202
 def coi_view(request):
62 203
     pprint(f"xxxx method = xxx {request.method}")
204
+    users = User.objects.all()
205
+
63 206
     if request.method == "POST":
207
+        pprint(request.POST)
208
+        exports = request.POST.getlist("exports")  # Retrieve the list of selected values
209
+        pprint(f"Selected Export Options: {exports}")
210
+        
211
+        if 'export' in request.POST:
212
+
213
+            data = {
214
+                "customer": "Tum Coder",
215
+                "inspect_date": "2025-01-15",
216
+                "lot_no": "12345",
217
+                "staff_name":  "Tum 8888",
218
+                "man_name":  "Tum 999",
219
+                "size": "Large",
220
+                "lot_size": "10 pcs",
221
+                "spec": "Spec-A",
222
+                "hardness_out.d1_act": "10",
223
+                "hardness_out.d2_act": "0[24:28]",  # Hide rows 24 to 28 if the prefix is "0"
224
+                "hardness_out.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
225
+                "hardness_out.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
226
+                "dimension_app.d1_act": "33",
227
+                "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
228
+                "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
229
+                "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
230
+            }
231
+            output_file = gen_xlsx(
232
+                template_file="/app/report/coi_templates.xlsx",
233
+                selected_sheets=exports,  # Replace with your actual sheet names
234
+                prefix_filename="/app/media/coi",
235
+                data=data
236
+            )
237
+            report = Report.objects.create(
238
+                name=request.POST.get('lot_no','Untitled'),
239
+                created_by=request.user,
240
+                file=None  # Leave this as None or assign a file if required
241
+            )
242
+            output_file_path = Path(output_file)  # Convert to a Path object for convenience
243
+            with open(output_file_path, "rb") as f:
244
+                report.file.save(output_file_path.name, File(f), save=True)
245
+
246
+            pprint(f"outputfile = {output_file}")
247
+
64 248
         if 'search_lot' in request.POST:
65 249
             lot_no = request.POST.get('lot_no', None)
66 250
             lot_no = lot_no.strip()
@@ -78,8 +262,44 @@ def coi_view(request):
78 262
                                                            'size_str': size_str,
79 263
                                                            'spec': spec})
80 264
 
81
-        exports = request.POST.getlist("exports")  # Retrieve the list of selected values
82
-        pprint(f"Selected Export Options: {exports}")
83 265
         messages.success(request, "Request Sent")
84 266
         return redirect(request.path_info)
85
-    return render(request, 'report/coi.html')
267
+    return render(request, 'report/coi.html', {'SHEET_NAMES': SHEET_NAMES, 'users': users})
268
+
269
+
270
+@csrf_exempt  # Disable CSRF for API requests (ensure this is secure in production)
271
+@login_required
272
+def gen_report_view(request):
273
+    if request.method == "POST":
274
+        try:
275
+            # Parse JSON data from the request body
276
+            data = json.loads(request.body)
277
+            lot_no = data.get("lot_no").strip()
278
+            exports = data.get("exports")
279
+            qa1 = data.get('qa1')
280
+            qa2 = data.get('qa2')
281
+            print(f"data = {data}")
282
+
283
+            if not lot_no:
284
+                return HttpResponseBadRequest("Missing 'lot_no' in request data")
285
+
286
+            # Call the `create_coi_file` function with the provided lot_no
287
+            report = create_coi_file(lot_no, exports, request.user, {'qa1': qa1, 'qa2': qa2})
288
+
289
+            # Return a success response with the report details
290
+            return JsonResponse({
291
+                "message": "Report generated successfully",
292
+                "report_id": report.id,
293
+                "file_url": report.file.url if report.file else None,
294
+            })
295
+
296
+        except json.JSONDecodeError:
297
+            return HttpResponseBadRequest("Invalid JSON data")
298
+        except Exception as e:
299
+            pprint(e)
300
+            return JsonResponse({"error": str(e)}, status=500)
301
+    else:
302
+        return HttpResponseBadRequest("Only POST requests are allowed")
303
+
304
+
305
+

~$hardness.xlsx → app/report/~$coi_templates.xlsx


+ 3 - 0
app/sysadmin/apps.py

@@ -4,3 +4,6 @@ from django.apps import AppConfig
4 4
 class SysadminConfig(AppConfig):
5 5
     default_auto_field = 'django.db.models.BigAutoField'
6 6
     name = 'sysadmin'
7
+
8
+    def ready(self):
9
+        import sysadmin.signals

+ 12 - 0
app/sysadmin/filters.py

@@ -0,0 +1,12 @@
1
+
2
+import django_filters
3
+from django.contrib.auth.models import User
4
+
5
+class UserFilter(django_filters.FilterSet):
6
+    username = django_filters.CharFilter(field_name='username', lookup_expr='icontains')
7
+    first_name = django_filters.CharFilter(field_name='first_name', lookup_expr='icontains')
8
+    last_name = django_filters.CharFilter(field_name='last_name', lookup_expr='icontains')
9
+
10
+    class Meta:
11
+        model = User
12
+        fields = ['username', 'first_name', 'last_name']  # Add fields you want to filter

+ 61 - 2
app/sysadmin/forms.py

@@ -2,6 +2,11 @@
2 2
 from django import forms
3 3
 from django.contrib.auth.forms import AuthenticationForm
4 4
 
5
+from django.contrib.auth.forms import UserCreationForm
6
+from django.contrib.auth.models import User
7
+
8
+from .models import UserProfile
9
+
5 10
 class CustomLoginForm(AuthenticationForm):
6 11
     username = forms.CharField(widget=forms.TextInput(attrs={
7 12
         'placeholder': 'Username'
@@ -11,8 +16,6 @@ class CustomLoginForm(AuthenticationForm):
11 16
     }))
12 17
 
13 18
 # forms.py
14
-from django.contrib.auth.forms import UserCreationForm
15
-from django.contrib.auth.models import User
16 19
 
17 20
 class CustomUserCreationForm(UserCreationForm):
18 21
     class Meta:
@@ -33,3 +36,59 @@ class CustomUserCreationForm(UserCreationForm):
33 36
         self.fields['password2'].widget.attrs.update({
34 37
             'placeholder': 'Confirm Password'
35 38
         })
39
+
40
+
41
+class UserProfileForm(forms.ModelForm):
42
+    class Meta:
43
+        model = UserProfile
44
+        fields = ['profile_picture', 'position', 'signed_picture']  # Include the fields you want to manage
45
+
46
+class UserCustomForm(forms.ModelForm):
47
+    # Profile fields
48
+    profile_picture = forms.ImageField(required=False, label="Profile Picture")
49
+    signed_picture = forms.ImageField(required=False, label="Signed Picture")
50
+    position = forms.ChoiceField(
51
+        choices=UserProfile.POSITION_CHOICES, 
52
+        required=False, 
53
+        label="Position"
54
+    )
55
+
56
+    class Meta:
57
+        model = User
58
+        fields = [
59
+            "first_name",
60
+            "last_name",
61
+            "is_staff",
62
+            "is_superuser",
63
+            "is_active",
64
+        ]
65
+
66
+    def __init__(self, *args, **kwargs):
67
+        # Allow passing `instance` to access both User and UserProfile objects
68
+        user_instance = kwargs.pop('instance', None)
69
+        profile_instance = getattr(user_instance, 'profile', None)
70
+        super().__init__(instance=user_instance, *args, **kwargs)
71
+
72
+        # Populate initial data for profile fields if `profile` exists
73
+        if profile_instance:
74
+            self.fields['profile_picture'].initial = profile_instance.profile_picture
75
+            self.fields['signed_picture'].initial = profile_instance.signed_picture
76
+            self.fields['position'].initial = profile_instance.position
77
+
78
+    def save(self, commit=True):
79
+        # Save the User instance first
80
+        user = super().save(commit=commit)
81
+        profile_data = {
82
+            'profile_picture': self.cleaned_data.get('profile_picture'),
83
+            'signed_picture': self.cleaned_data.get('signed_picture'),
84
+            'position': self.cleaned_data.get('position'),
85
+        }
86
+
87
+        # Ensure profile exists for the user
88
+        profile, created = UserProfile.objects.get_or_create(user=user)
89
+        for key, value in profile_data.items():
90
+            setattr(profile, key, value)
91
+        if commit:
92
+            profile.save()
93
+
94
+        return user

+ 29 - 0
app/sysadmin/migrations/0001_initial.py

@@ -0,0 +1,29 @@
1
+# Generated by Django 4.2 on 2025-01-17 04:58
2
+
3
+from django.conf import settings
4
+from django.db import migrations, models
5
+import django.db.models.deletion
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='UserProfile',
19
+            fields=[
20
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+                ('bio', models.TextField(blank=True, null=True)),
22
+                ('profile_picture', models.ImageField(blank=True, null=True, upload_to='profile/%Y/%m/%d/')),
23
+                ('signed_picture', models.ImageField(blank=True, null=True, upload_to='signed/%Y/%m/%d/')),
24
+                ('email', models.EmailField(blank=True, max_length=254, null=True)),
25
+                ('position', models.CharField(blank=True, choices=[('QA_STAFF', 'QA Staff'), ('QA_MANAGER', 'QA Manager')], max_length=20, null=True)),
26
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
27
+            ],
28
+        ),
29
+    ]

+ 20 - 0
app/sysadmin/models.py

@@ -1,3 +1,23 @@
1 1
 from django.db import models
2 2
 
3 3
 # Create your models here.
4
+
5
+from django.contrib.auth.models import User
6
+
7
+class UserProfile(models.Model):
8
+    POSITION_CHOICES = [
9
+        ('QA_STAFF', 'QA Staff'),
10
+        ('QA_MANAGER', 'QA. MG.'),
11
+        ('QA_AST_MANAGER', 'QA. Asst. MG.'),
12
+        ('QA_ENGINEER', 'QA. Engineer'),
13
+    ]
14
+    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
15
+    bio = models.TextField(blank=True, null=True)
16
+    profile_picture = models.ImageField(upload_to="profile/%Y/%m/%d/", blank=True, null=True)
17
+    signed_picture = models.ImageField(upload_to="signed/%Y/%m/%d/", blank=True, null=True)
18
+    email = models.EmailField(blank=True, null=True)  # New email field
19
+    position = models.CharField(max_length=20, choices=POSITION_CHOICES, blank=True, null=True)  # New position field
20
+
21
+    def __str__(self):
22
+        pos = self.get_position_display()
23
+        return f"{self.user.username} / {self.user.first_name} {self.user.last_name} #{pos}"

+ 13 - 0
app/sysadmin/signals.py

@@ -0,0 +1,13 @@
1
+from django.db.models.signals import post_save
2
+from django.dispatch import receiver
3
+from django.contrib.auth.models import User
4
+from .models import UserProfile
5
+
6
+@receiver(post_save, sender=User)
7
+def create_user_profile(sender, instance, created, **kwargs):
8
+    if created:
9
+        UserProfile.objects.create(user=instance)
10
+
11
+@receiver(post_save, sender=User)
12
+def save_user_profile(sender, instance, **kwargs):
13
+    instance.profile.save()

+ 94 - 0
app/sysadmin/templates/sysadmin/profile.html

@@ -0,0 +1,94 @@
1
+{% extends "base.html" %}
2
+{% load tailwind_filters %}
3
+
4
+{% block title %}Report Dashboard{% endblock %}
5
+{% block content %}
6
+<div class="container mx-auto px-4 py-8">
7
+<h1 class="text-2xl font-bold text-gray-700 mb-6">Your Profile</h1>
8
+
9
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
10
+            <!-- Username -->
11
+            <div class="flex items-center bg-gray-50 p-4 rounded-lg shadow-sm border">
12
+                <div class="flex-shrink-0 bg-indigo-100 rounded-full h-12 w-12 flex items-center justify-center">
13
+                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
14
+  <path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
15
+</svg>
16
+
17
+                </div>
18
+                <div class="ml-4">
19
+                    <h3 class="text-lg font-medium text-gray-900">Username</h3>
20
+                    <p class="text-gray-600">{{ user.username }}</p>
21
+                </div>
22
+            </div>
23
+
24
+            <!-- Email -->
25
+            <div class="flex items-center bg-gray-50 p-4 rounded-lg shadow-sm border">
26
+                <div class="flex-shrink-0 bg-indigo-100 rounded-full h-12 w-12 flex items-center justify-center">
27
+                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
28
+  <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
29
+</svg>
30
+
31
+                </div>
32
+                <div class="ml-4">
33
+                    <h3 class="text-lg font-medium text-gray-900">Email</h3>
34
+                    <p class="text-gray-600">{{ user.email }}</p>
35
+                </div>
36
+            </div>
37
+        </div>
38
+<form method="POST" enctype="multipart/form-data" class='space-y-4'>
39
+    {% csrf_token %}
40
+              <!-- First Name -->
41
+              <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
42
+
43
+                <div>
44
+                  <label for="first_name" class="block text-sm font-medium text-gray-700">First Name</label>
45
+                  <input
46
+                      type="text"
47
+                      name="first_name"
48
+                      id="first_name"
49
+                      value="{{ user.first_name }}"
50
+                      class="block w-full mt-2 rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
51
+                      />
52
+                </div>
53
+
54
+                <!-- Last Name -->
55
+                <div>
56
+                  <label for="last_name" class="block text-sm font-medium text-gray-700">Last Name</label>
57
+                  <input
58
+                      type="text"
59
+                      name="last_name"
60
+                      id="last_name"
61
+                      value="{{ user.last_name }}"
62
+                      class="block w-full mt-2 rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
63
+                      />
64
+                </div>
65
+              </div>
66
+    {% if form.instance.profile_picture %}
67
+    <!-- Show uploaded profile picture -->
68
+    <img src="{{ form.instance.profile_picture.url }}" 
69
+         alt="Profile Picture" 
70
+         class="w-32 h-32 rounded-full mb-4 shadow-md">
71
+    {% else %}
72
+    <!-- Placeholder image if no profile picture -->
73
+    <div class="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 mb-4">
74
+      <span>No Image</span>
75
+    </div>
76
+    {% endif %}
77
+    {{ form.profile_picture }}
78
+    {% if form.instance.signed_picture %}
79
+    <!-- Show uploaded profile picture -->
80
+    <img src="{{ form.instance.signed_picture.url }}" 
81
+         alt="Profile Picture" 
82
+         class="w-32 mb-4 shadow-md">
83
+    {% else %}
84
+    <!-- Placeholder image if no profile picture -->
85
+    <div class="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 mb-4">
86
+      <span>No Image</span>
87
+    </div>
88
+    {% endif %}
89
+    {{ form.signed_picture }}
90
+    {{ form.position | as_crispy_field }}
91
+    <button type="submit" class="px-6 py-2 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">Save Changes</button>
92
+</form>
93
+</div>
94
+{% endblock %}

+ 71 - 0
app/sysadmin/templates/sysadmin/user_form.html

@@ -0,0 +1,71 @@
1
+{% extends "base.html" %}
2
+
3
+{% load legacy_filters %}
4
+{% load tailwind_filters %}
5
+{% load crispy_forms_filters %}
6
+
7
+{% block title %}
8
+    {% if view.title %}
9
+        {{ view.title }}
10
+    {% else %}
11
+        {{ view|class_name }}
12
+    {% endif %}
13
+{% endblock %}
14
+
15
+{% block content %}
16
+<div class="container mx-auto px-4 py-6">
17
+    <h1 class="text-2xl font-bold mb-6">
18
+        {% if view.title %}
19
+            {{ view.title }}
20
+        {% elif view|class_name == "CreateViewClass" %}
21
+            Create {{ model_verbose_name }}
22
+        {% else %}
23
+            Update {{ model_verbose_name }}
24
+        {% endif %}
25
+    </h1>
26
+
27
+    <!-- Render the Form -->
28
+    <form method="post" enctype="multipart/form-data">
29
+        {% csrf_token %}
30
+        <div class="flex flex-wrap items-center space-x-4">
31
+            <!-- Show profile_picture -->
32
+            <div class=" mb-4">
33
+                {% if form.instance.profile and form.instance.profile.profile_picture %}
34
+                    <div class="mb-2">
35
+                        <img src="{{ form.instance.profile.profile_picture.url }}" alt="Profile Picture" class="max-w-xs border rounded" width=100>
36
+                    </div>
37
+                {% endif %}
38
+                {{ form.profile_picture|as_crispy_field }}
39
+            </div>
40
+
41
+            <!-- Show signed_picture -->
42
+            <div class=" mb-4">
43
+                {% if form.instance.profile and form.instance.profile.signed_picture %}
44
+                    <div class="mb-2">
45
+                        <img src="{{ form.instance.profile.signed_picture.url }}" alt="Signed Picture" class="max-w-xs border rounded" width=100>
46
+                    </div>
47
+                {% endif %}
48
+                {{ form.signed_picture|as_crispy_field }}
49
+            </div>
50
+
51
+            <!-- Render other form fields -->
52
+            {% for field in form %}
53
+                {% if field.name not in "profile_picture signed_picture" %}
54
+                    <div class=" mb-4">
55
+                        {{ field|as_crispy_field }}
56
+                    </div>
57
+                {% endif %}
58
+            {% endfor %}
59
+        </div>
60
+
61
+        <div class="mt-4">
62
+            <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
63
+                Save
64
+            </button>
65
+            <a href="{% url list_url_name %}" class="bg-gray-300 text-gray-800 px-4 py-2 rounded hover:bg-gray-400">
66
+                Cancel
67
+            </a>
68
+        </div>
69
+    </form>
70
+</div>
71
+{% endblock %}

+ 8 - 0
app/sysadmin/urls.py

@@ -3,8 +3,16 @@ from . import views
3 3
 
4 4
 app_name = "sysadmin"  # Use this namespace for reverse URL lookups
5 5
 
6
+users_crud = views.UserCRUDView()
7
+
6 8
 urlpatterns = [
7 9
     path('login/', views.login_view, name='login'),
8 10
     path('register/', views.register_view, name='register'),
9 11
     path('logout/', views.logout_view, name='logout'),
12
+    path('profile/', views.profile_view, name='profile'),  # Add profile view URL
13
+
14
+    path('users/', users_crud.get_list_view().as_view(), name='users-list'),
15
+    path('users/create/', users_crud.get_create_view().as_view(), name='users-create'),
16
+    path('users/<int:pk>/update/', users_crud.get_update_view().as_view(), name='users-update'),
17
+    path('users/<int:pk>/delete/', users_crud.get_delete_view().as_view(), name='users-delete'),
10 18
 ]

+ 57 - 1
app/sysadmin/views.py

@@ -1,7 +1,13 @@
1 1
 # views.py
2 2
 from django.contrib.auth import authenticate, login, logout
3 3
 from django.shortcuts import render, redirect
4
-from .forms import CustomLoginForm, CustomUserCreationForm
4
+from .forms import CustomLoginForm, CustomUserCreationForm, UserProfileForm, UserCustomForm
5
+from .models import UserProfile
6
+from .filters import UserFilter
7
+from django.contrib.auth.decorators import login_required
8
+from django.contrib import messages
9
+from core.utils import ConfigurableCRUDView
10
+from django.contrib.auth.models import User
5 11
 
6 12
 def login_view(request):
7 13
     if request.method == "POST":
@@ -30,3 +36,53 @@ def register_view(request):
30 36
 def logout_view(request):
31 37
     logout(request)  # Logs out the user
32 38
     return redirect('sysadmin:login')  # Redirect to the login page after logout
39
+
40
+@login_required
41
+def profile_view(request):
42
+    try:
43
+        # Get the profile for the current user
44
+        profile = request.user.profile
45
+    except UserProfile.DoesNotExist:
46
+        # Create a profile if it doesn't exist
47
+        profile = UserProfile.objects.create(user=request.user)
48
+
49
+    if request.method == "POST":
50
+        form = UserProfileForm(request.POST, request.FILES, instance=profile)
51
+
52
+        user = request.user
53
+        user.first_name = request.POST.get('first_name', user.first_name)
54
+        user.last_name = request.POST.get('last_name', user.last_name)
55
+
56
+        if form.is_valid():
57
+            form.save()
58
+            user.save()
59
+            messages.success(request, "Profile Updated")
60
+            return redirect('sysadmin:profile')  # Redirect to the profile page after saving
61
+        else:
62
+            messages.error(request, form.errors)
63
+    else:
64
+        form = UserProfileForm(instance=profile)
65
+
66
+    return render(request, 'sysadmin/profile.html', {'form': form})
67
+
68
+
69
+class UserCRUDView(ConfigurableCRUDView):
70
+    model = User
71
+    list_template_name = 'legacy/datacrud_list.html'
72
+    detail_template_name = 'legacy/datacrud_detail.html'
73
+    form_template_name = 'sysadmin/user_form.html'
74
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
75
+    filterset_class = UserFilter
76
+
77
+    page_title = "Users"
78
+
79
+    # URL name mappings
80
+    list_url_name = 'sysadmin:users-list'
81
+    create_url_name = 'sysadmin:users-create'
82
+    update_url_name = 'sysadmin:users-update'
83
+    delete_url_name = 'sysadmin:users-delete'
84
+    # excludes = ["splitdata"]
85
+    config_fields = ("id", "username", "is_active", "is_staff", "is_superuser", "last_login")
86
+    config_field_orders = ["id",]  # Display these fields first
87
+    form_class = UserCustomForm
88
+    config_edit_fields = None

+ 44 - 12
app/templates/base.html

@@ -7,7 +7,7 @@
7 7
 <head>
8 8
     <meta charset="UTF-8">
9 9
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
-    <title>{% block title %}My Django App{% endblock %}</title>
10
+    <title>{% block title %}COI System{% endblock %}</title>
11 11
 
12 12
     <!-- TailwindCSS -->
13 13
     <!--
@@ -16,6 +16,7 @@
16 16
     <link href="{% static "font-awesome/css/font-awesome.css" %}" rel="stylesheet" />
17 17
     <script type="text/javascript" defer src="{% static "alpinejs/dist/cdn.min.js" %}"></script>
18 18
     <link href="https://cdn.jsdelivr.net/npm/flowbite@2.5.2/dist/flowbite.min.css" rel="stylesheet" />
19
+    <script type="text/javascript"  src="{% static "axios/dist/axios.js" %}"></script>
19 20
     <script type="text/javascript"  src="{% static "js/main.js" %}"></script>
20 21
 </head>
21 22
 <body class="bg-gray-100 text-gray-800">
@@ -39,7 +40,7 @@
39 40
 
40 41
                 {% if user.is_authenticated %}
41 42
                     <!-- Logged-in User -->
42
-                    <span class="text-gray-600 dark:text-gray-300">Hello, {{ user.username }}</span>
43
+                    <a href="{% url 'sysadmin:profile' %}"><span class="text-gray-600 dark:text-gray-300">Hello, {{ user.username }}</span></a>
43 44
                     <a href="{% url 'sysadmin:logout' %}" class="text-blue-500 hover:underline">Logout</a>
44 45
                 {% else %}
45 46
                     <!-- Guest User -->
@@ -81,6 +82,15 @@
81 82
                             <a href="{% url "legacy:data-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data</a>
82 83
                           </li>
83 84
                           <li>
85
+                            <a href="{% url "legacy:datams-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data MS</a>
86
+                          </li>
87
+                          <li>
88
+                            <a href="{% url "legacy:datarl-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data RL</a>
89
+                          </li>
90
+                          <li>
91
+                            <a href="{% url "legacy:datawb-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Data WB</a>
92
+                          </li>
93
+                          <li>
84 94
                             <a href="{% url "legacy:datams-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Measurement</a>
85 95
                           </li>
86 96
                           <li>
@@ -90,6 +100,12 @@
90 100
                             <a href="{% url "legacy:ls-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Lot Summary</a>
91 101
                           </li>
92 102
                           <li>
103
+                            <a href="{% url "legacy:lsrl-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Lot Summary RL</a>
104
+                          </li>
105
+                          <li>
106
+                            <a href="{% url "legacy:lswb-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Lot Summary WB</a>
107
+                          </li>
108
+                          <li>
93 109
                             <a href="{% url "legacy:vm-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">VMaster</a>
94 110
                           </li>
95 111
                           <li>
@@ -101,12 +117,26 @@
101 117
                           <li>
102 118
                             <a href="{% url "legacy:em-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">EMaster</a>
103 119
                           </li>
104
-                          <li>
105
-                             <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Invoice</a>
106
-                          </li>
107 120
                     </ul>
108 121
                  </li>
109
-                <li><a href="/settings/" class="flex items-center p-2 text-gray-900 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white"><span class="ml-3">Settings</span></a></li>
122
+                 <li>
123
+
124
+                   <button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="setting-sub" data-collapse-toggle="setting-sub">
125
+                     <svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
126
+                       <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 6c0 1.657-3.134 3-7 3S5 7.657 5 6m14 0c0-1.657-3.134-3-7-3S5 4.343 5 6m14 0v6M5 6v6m0 0c0 1.657 3.134 3 7 3s7-1.343 7-3M5 12v6c0 1.657 3.134 3 7 3s7-1.343 7-3v-6"/>
127
+                     </svg>
128
+
129
+                     <span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
130
+                     <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
131
+                       <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
132
+                     </svg>
133
+                   </button>
134
+                   <ul id="setting-sub" class="hidden py-2 space-y-2">
135
+                     <li>
136
+                       <a href="{% url "sysadmin:users-list" %}" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">Users</a>
137
+                     </li>
138
+                   </ul>
139
+                 </li>
110 140
             </ul>
111 141
         </div>
112 142
     </aside>
@@ -144,14 +174,16 @@
144 174
 
145 175
     {% django_browser_reload_script %}
146 176
     <script>
147
-    // Auto-hide the message after 5 seconds
148
-    setTimeout(() => {
177
+      // Auto-hide the message after 5 seconds
178
+      setTimeout(() => {
149 179
         const alert = document.getElementById('message-alert');
150 180
         if (alert) {
151
-            alert.style.opacity = '0'; // Fade out
152
-            setTimeout(() => alert.remove(), 500); // Remove after fade-out
181
+          alert.style.opacity = '0'; // Fade out
182
+          setTimeout(() => alert.remove(), 500); // Remove after fade-out
153 183
         }
154
-    }, 5000); // 5 seconds
155
-</script>
184
+      }, 5000); // 5 seconds
185
+    </script>
186
+    {% block footer_script %}
187
+    {% endblock %}
156 188
 </body>
157 189
 </html>

BIN
hardness.xlsx


+ 3 - 1
test_xlsx2.py

@@ -139,6 +139,8 @@ if __name__ == "__main__":
139 139
         "customer": "Tum Coder",
140 140
         "inspect_date": "2025-01-15",
141 141
         "lot_no": "12345",
142
+        "staff_name":  "Tum 8888",
143
+        "man_name":  "Tum 999",
142 144
         "size": "Large",
143 145
         "pcs": "10 pcs",
144 146
         "spec": "Spec-A",
@@ -149,7 +151,7 @@ if __name__ == "__main__":
149 151
         "dimension_app.d1_act": "33",
150 152
         "dimension_app.d2_act": "0[26:32]",  # Hide rows 24 to 28 if the prefix is "0"
151 153
         "dimension_app.acc": True,  # Hide rows 24 to 28 if the prefix is "0"
152
-        "dimension_app.spe_acc": False,  # Hide rows 24 to 28 if the prefix is "0"
154
+        "dimension_app.spe_acc": True,  # Hide rows 24 to 28 if the prefix is "0"
153 155
     }
154 156
 
155 157
 

golf/tge - Gogs: Simplico Git Service

Ei kuvausta

golf d8e79ace03 index commit 2 vuotta sitten
..
LICENSE d8e79ace03 index commit 2 vuotta sitten
README.md d8e79ace03 index commit 2 vuotta sitten
changelog.md d8e79ace03 index commit 2 vuotta sitten
common.js d8e79ace03 index commit 2 vuotta sitten
glob.js d8e79ace03 index commit 2 vuotta sitten
package.json d8e79ace03 index commit 2 vuotta sitten
sync.js d8e79ace03 index commit 2 vuotta sitten

README.md

Glob

Match files using the patterns the shell uses, like stars and stuff.

Build Status Build Status Coverage Status

This is a glob implementation in JavaScript. It uses the minimatch library to do its matching.

Usage

Install with npm

npm i glob
var glob = require("glob")

// options is optional
glob("**/*.js", options, function (er, files) {
  // files is an array of filenames.
  // If the `nonull` option is set, and nothing
  // was found, then files is ["**/*.js"]
  // er is an error object or null.
})

Glob Primer

"Globs" are the patterns you type when you do stuff like ls *.js on the command line, or put build/* in a .gitignore file.

Before parsing the path part patterns, braced sections are expanded into a set. Braced sections start with { and end with }, with any number of comma-delimited sections within. Braced sections may contain slash characters, so a{/b/c,bcd} would expand into a/b/c and abcd.

The following characters have special magic meaning when used in a path portion:

  • * Matches 0 or more characters in a single path portion
  • ? Matches 1 character
  • [...] Matches a range of characters, similar to a RegExp range. If the first character of the range is ! or ^ then it matches any character not in the range.
  • !(pattern|pattern|pattern) Matches anything that does not match any of the patterns provided.
  • ?(pattern|pattern|pattern) Matches zero or one occurrence of the patterns provided.
  • +(pattern|pattern|pattern) Matches one or more occurrences of the patterns provided.
  • *(a|b|c) Matches zero or more occurrences of the patterns provided
  • @(pattern|pat*|pat?erN) Matches exactly one of the patterns provided
  • ** If a "globstar" is alone in a path portion, then it matches zero or more directories and subdirectories searching for matches. It does not crawl symlinked directories.

Dots

If a file or directory path portion has a . as the first character, then it will not match any glob pattern unless that pattern's corresponding path part also has a . as its first character.

For example, the pattern a/.*/c would match the file at a/.b/c. However the pattern a/*/c would not, because * does not start with a dot character.

You can make glob treat dots as normal characters by setting dot:true in the options.

Basename Matching

If you set matchBase:true in the options, and the pattern has no slashes in it, then it will seek for any file anywhere in the tree with a matching basename. For example, *.js would match test/simple/basic.js.

Empty Sets

If no matching files are found, then an empty array is returned. This differs from the shell, where the pattern itself is returned. For example:

$ echo a*s*d*f
a*s*d*f

To get the bash-style behavior, set the nonull:true in the options.

See Also:

glob.hasMagic(pattern, [options])

Returns true if there are any special characters in the pattern, and false otherwise.

Note that the options affect the results. If noext:true is set in the options object, then +(a|b) will not be considered a magic pattern. If the pattern has a brace expansion, like a/{b/c,x/y} then that is considered magical, unless nobrace:true is set in the options.

glob(pattern, [options], cb)

  • pattern {String} Pattern to be matched
  • options {Object}
  • cb {Function}
    • err {Error | null}
    • matches {Array<String>} filenames found matching the pattern

Perform an asynchronous glob search.

glob.sync(pattern, [options])

  • pattern {String} Pattern to be matched
  • options {Object}
  • return: {Array<String>} filenames found matching the pattern

Perform a synchronous glob search.

Class: glob.Glob

Create a Glob object by instantiating the glob.Glob class.

var Glob = require("glob").Glob
var mg = new Glob(pattern, options, cb)

It's an EventEmitter, and starts walking the filesystem to find matches immediately.

new glob.Glob(pattern, [options], [cb])

  • pattern {String} pattern to search for
  • options {Object}
  • cb {Function} Called when an error occurs, or matches are found
    • err {Error | null}
    • matches {Array<String>} filenames found matching the pattern

Note that if the sync flag is set in the options, then matches will be immediately available on the g.found member.

Properties

  • minimatch The minimatch object that the glob uses.
  • options The options object passed in.
  • aborted Boolean which is set to true when calling abort(). There is no way at this time to continue a glob search after aborting, but you can re-use the statCache to avoid having to duplicate syscalls.
  • cache Convenience object. Each field has the following possible values:
    • false - Path does not exist
    • true - Path exists
    • 'FILE' - Path exists, and is not a directory
    • 'DIR' - Path exists, and is a directory
    • [file, entries, ...] - Path exists, is a directory, and the array value is the results of fs.readdir
  • statCache Cache of fs.stat results, to prevent statting the same path multiple times.
  • symlinks A record of which paths are symbolic links, which is relevant in resolving ** patterns.
  • realpathCache An optional object which is passed to fs.realpath to minimize unnecessary syscalls. It is stored on the instantiated Glob object, and may be re-used.

Events

  • end When the matching is finished, this is emitted with all the matches found. If the nonull option is set, and no match was found, then the matches list contains the original pattern. The matches are sorted, unless the nosort flag is set.
  • match Every time a match is found, this is emitted with the specific thing that matched. It is not deduplicated or resolved to a realpath.
  • error Emitted when an unexpected error is encountered, or whenever any fs error occurs if options.strict is set.
  • abort When abort() is called, this event is raised.

Methods

  • pause Temporarily stop the search
  • resume Resume the search
  • abort Stop the search forever

Options

All the options that can be passed to Minimatch can also be passed to Glob to change pattern matching behavior. Also, some have been added, or have glob-specific ramifications.

All options are false by default, unless otherwise noted.

All options are added to the Glob object, as well.

If you are running many glob operations, you can pass a Glob object as the options argument to a subsequent operation to shortcut some stat and readdir calls. At the very least, you may pass in shared symlinks, statCache, realpathCache, and cache options, so that parallel glob operations will be sped up by sharing information about the filesystem.

  • cwd The current working directory in which to search. Defaults to process.cwd().
  • root The place where patterns starting with / will be mounted onto. Defaults to path.resolve(options.cwd, "/") (/ on Unix systems, and C:\ or some such on Windows.)
  • dot Include .dot files in normal matches and globstar matches. Note that an explicit dot in a portion of the pattern will always match dot files.
  • nomount By default, a pattern starting with a forward-slash will be "mounted" onto the root setting, so that a valid filesystem path is returned. Set this flag to disable that behavior.
  • mark Add a / character to directory matches. Note that this requires additional stat calls.
  • nosort Don't sort the results.
  • stat Set to true to stat all results. This reduces performance somewhat, and is completely unnecessary, unless readdir is presumed to be an untrustworthy indicator of file existence.
  • silent When an unusual error is encountered when attempting to read a directory, a warning will be printed to stderr. Set the silent option to true to suppress these warnings.
  • strict When an unusual error is encountered when attempting to read a directory, the process will just continue on in search of other matches. Set the strict option to raise an error in these cases.
  • cache See cache property above. Pass in a previously generated cache object to save some fs calls.
  • statCache A cache of results of filesystem information, to prevent unnecessary stat calls. While it should not normally be necessary to set this, you may pass the statCache from one glob() call to the options object of another, if you know that the filesystem will not change between calls. (See "Race Conditions" below.)
  • symlinks A cache of known symbolic links. You may pass in a previously generated symlinks object to save lstat calls when resolving ** matches.
  • sync DEPRECATED: use glob.sync(pattern, opts) instead.
  • nounique In some cases, brace-expanded patterns can result in the same file showing up multiple times in the result set. By default, this implementation prevents duplicates in the result set. Set this flag to disable that behavior.
  • nonull Set to never return an empty set, instead returning a set containing the pattern itself. This is the default in glob(3).
  • debug Set to enable debug logging in minimatch and glob.
  • nobrace Do not expand {a,b} and {1..3} brace sets.
  • noglobstar Do not match ** against multiple filenames. (Ie, treat it as a normal * instead.)
  • noext Do not match +(a|b) "extglob" patterns.
  • nocase Perform a case-insensitive match. Note: on case-insensitive filesystems, non-magic patterns will match by default, since stat and readdir will not raise errors.
  • matchBase Perform a basename-only match if the pattern does not contain any slash characters. That is, *.js would be treated as equivalent to **/*.js, matching all js files in all directories.
  • nodir Do not match directories, only files. (Note: to match only directories, simply put a / at the end of the pattern.)
  • ignore Add a pattern or an array of glob patterns to exclude matches. Note: ignore patterns are always in dot:true mode, regardless of any other settings.
  • follow Follow symlinked directories when expanding ** patterns. Note that this can result in a lot of duplicate references in the presence of cyclic links.
  • realpath Set to true to call fs.realpath on all of the results. In the case of a symlink that cannot be resolved, the full absolute path to the matched entry is returned (though it will usually be a broken symlink)
  • absolute Set to true to always receive absolute paths for matched files. Unlike realpath, this also affects the values returned in the match event.

Comparisons to other fnmatch/glob implementations

While strict compliance with the existing standards is a worthwhile goal, some discrepancies exist between node-glob and other implementations, and are intentional.

The double-star character ** is supported by default, unless the noglobstar flag is set. This is supported in the manner of bsdglob and bash 4.3, where ** only has special significance if it is the only thing in a path part. That is, a/**/b will match a/x/y/b, but a/**b will not.

Note that symlinked directories are not crawled as part of a **, though their contents may match against subsequent portions of the pattern. This prevents infinite loops and duplicates and the like.

If an escaped pattern has no matches, and the nonull flag is set, then glob returns the pattern as-provided, rather than interpreting the character escapes. For example, glob.match([], "\\*a\\?") will return "\\*a\\?" rather than "*a?". This is akin to setting the nullglob option in bash, except that it does not resolve escaped pattern characters.

If brace expansion is not disabled, then it is performed before any other interpretation of the glob pattern. Thus, a pattern like +(a|{b),c)}, which would not be valid in bash or zsh, is expanded first into the set of +(a|b) and +(a|c), and those patterns are checked for validity. Since those two are valid, matching proceeds.

Comments and Negation

Previously, this module let you mark a pattern as a "comment" if it started with a # character, or a "negated" pattern if it started with a ! character.

These options were deprecated in version 5, and removed in version 6.

To specify things that should not match, use the ignore option.

Windows

Please only use forward-slashes in glob expressions.

Though windows uses either / or \ as its path separator, only / characters are used by this glob implementation. You must use forward-slashes only in glob expressions. Back-slashes will always be interpreted as escape characters, not path separators.

Results from absolute patterns such as /foo/* are mounted onto the root setting using path.join. On windows, this will by default result in /foo/* matching C:\foo\bar.txt.

Race Conditions

Glob searching, by its very nature, is susceptible to race conditions, since it relies on directory walking and such.

As a result, it is possible that a file that exists when glob looks for it may have been deleted or modified by the time it returns the result.

As part of its internal implementation, this program caches all stat and readdir calls that it makes, in order to cut down on system overhead. However, this also makes it even more susceptible to races, especially if the cache or statCache objects are reused between glob calls.

Users are thus advised not to use a glob result as a guarantee of filesystem state in the face of rapid changes. For the vast majority of operations, this is never a problem.

Glob Logo

Glob's logo was created by Tanya Brassie. Logo files can be found here.

The logo is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Contributing

Any change to behavior (including bugfixes) must come with a test.

Patches that fail tests or reduce performance will be rejected.

# to run tests
npm test

# to re-generate test fixtures
npm run test-regen

# to benchmark against bash/zsh
npm run bench

# to profile javascript
npm run prof