-num-new"> 507
+    code_no = models.CharField(max_length=100, null=True, blank=True)
508
+    code_no_mks = models.CharField(max_length=100, null=True, blank=True)
509
+    lot_no = models.CharField(max_length=100, null=True, blank=True)
510
+    drawing = models.ImageField(upload_to='drawings/')
511
+    description = models.TextField(null=True, blank=True)
512
+    created_at = models.DateTimeField(auto_now_add=True)
513
+    updated_at = models.DateTimeField(auto_now=True)
514
+
515
+    created_by = models.ForeignKey(
516
+        User,
517
+        on_delete=models.SET_NULL,
518
+        null=True,
519
+        blank=True, 
520
+    )  # Reference to the user who created the report
521
+    
522
+    def __str__(self):
523
+        return f"{self.code_no} - {self.lot_no}"

+ 14 - 1
app/core/utils.py

@@ -12,6 +12,7 @@ from django.views.generic import (
12 12
 )
13 13
 from django.core.paginator import Paginator
14 14
 from core.models import MgMasterView, VMasterView, BelMasterView, EMasterView
15
+from pprint import pprint
15 16
 
16 17
 class ConfigurableCRUDView:
17 18
     model = None
@@ -44,6 +45,7 @@ class ConfigurableCRUDView:
44 45
         - Respects `config_field_orders` and `config_excludes`.
45 46
         Returns field instances instead of field names.
46 47
         """
48
+        pprint("get_fields")
47 49
         model_fields = {f.name: f for f in self.model._meta.get_fields()}
48 50
 
49 51
         # Filter based on `config_fields` configuration
@@ -65,7 +67,6 @@ class ConfigurableCRUDView:
65 67
         # Reorder fields to match the order specified in `config_field_orders`
66 68
         ordered_field_names = set(self.config_field_orders)
67 69
         ordered_fields.sort(key=lambda f: self.config_field_orders.index(f.name) if f.name in ordered_field_names else len(ordered_field_names))
68
-
69 70
         return ordered_fields + remaining_fields
70 71
 
71 72
 
@@ -226,3 +227,15 @@ def queryFromMaster(lot_no):
226 227
         results.extend(queryset)
227 228
     return results
228 229
 
230
+SHEET_NAMES = {
231
+    'hardness_out': 'Hardness Out',
232
+    'hardness_out_in': 'Hardness Out/In', 
233
+    'hardness_both_size': 'Hardness Both Size',
234
+    'dimension': 'Dimension',
235
+    'dimension_app': 'Dimension Appearance',
236
+    'dimension_bal_weight': 'Dimension Balance/Weight',
237
+    'dim_bal_app_hard': 'Dimension Balance/Appearance/Hardness',
238
+    'dim_bal_app_rot_hard': 'Dimension Balance/Appearance/Rotation/Hardness',
239
+    'thickness_8_point': 'Thickness 8 Points',
240
+    'centering': 'Centering',
241
+}

+ 104 - 0
app/legacy/migrations/0003_allproductaverageobminmaxview_and_more.py

@@ -0,0 +1,104 @@
1
+# Generated by Django 4.2 on 2025-05-06 04:36
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('legacy', '0002_belmasterview_emasterview_mgmasterview_vmasterview'),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.CreateModel(
14
+            name='AllProductAverageObMinMaxView',
15
+            fields=[
16
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17
+                ('ProductCode', models.CharField(max_length=255, null=True)),
18
+                ('out_min', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
19
+                ('out_max', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
20
+                ('in_min', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
21
+                ('in_max', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
22
+            ],
23
+            options={
24
+                'db_table': 'AllProduct_Average_OB_MIN_MAX_view',
25
+                'managed': False,
26
+            },
27
+        ),
28
+        migrations.CreateModel(
29
+            name='AllProductDimensionForInsProcess',
30
+            fields=[
31
+                ('ProdType', models.CharField(max_length=255, null=True)),
32
+                ('ProductCode', models.CharField(max_length=255, primary_key=True, serialize=False)),
33
+                ('Size_Id', models.CharField(max_length=255, null=True)),
34
+                ('Size_Name', models.CharField(max_length=255, null=True)),
35
+                ('Std', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
36
+                ('TolUn', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
37
+                ('TolUp', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
38
+            ],
39
+            options={
40
+                'db_table': 'AllProduct_Dimension_ForInsProcess',
41
+                'managed': False,
42
+            },
43
+        ),
44
+        migrations.CreateModel(
45
+            name='AllProductPressPositionPressWeight',
46
+            fields=[
47
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
48
+                ('ProductCode', models.CharField(max_length=255, null=True)),
49
+                ('Lot_No', models.CharField(max_length=255, null=True)),
50
+                ('PO_Qty', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
51
+                ('UWeight', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
52
+                ('Current_ProNo', models.CharField(max_length=255, null=True)),
53
+                ('Press_Time', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
54
+                ('PressType_1', models.CharField(max_length=255, null=True)),
55
+                ('PressWeight_1', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
56
+                ('PressType_2', models.CharField(max_length=255, null=True)),
57
+                ('PressWeight_2', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
58
+                ('Press_Ton', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
59
+                ('Press_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
60
+                ('Press_T_Tol', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
61
+                ('Mold_D', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
62
+                ('Mold_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
63
+                ('SegMold_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
64
+                ('SegMold_D', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
65
+                ('Center_D', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
66
+                ('Center_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
67
+                ('LowerPlate_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
68
+                ('StudPlate_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
69
+                ('UpperPlate_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
70
+                ('PinPlate_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
71
+                ('TopConcave_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
72
+                ('BottomConcave_T', models.DecimalField(decimal_places=2, max_digits=10, null=True)),
73
+            ],
74
+            options={
75
+                'db_table': 'AllProduct_PressPosition_PressWeight',
76
+                'managed': False,
77
+            },
78
+        ),
79
+        migrations.CreateModel(
80
+            name='RotateBrokenTest',
81
+            fields=[
82
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
83
+                ('row_no', models.IntegerField(blank=True, null=True)),
84
+                ('speed_spec', models.FloatField(blank=True, null=True)),
85
+                ('speed_test', models.FloatField(blank=True, null=True)),
86
+                ('speedOk', models.CharField(blank=True, max_length=4, null=True)),
87
+                ('qty', models.IntegerField()),
88
+                ('station_no', models.IntegerField()),
89
+                ('created_at', models.DateTimeField(blank=True, null=True)),
90
+                ('updated_at', models.DateTimeField(blank=True, null=True)),
91
+                ('lot_no', models.CharField(blank=True, max_length=50, null=True)),
92
+                ('machine_id', models.IntegerField(blank=True, null=True)),
93
+                ('code', models.CharField(blank=True, max_length=50, null=True)),
94
+                ('emp_id', models.IntegerField()),
95
+                ('devid', models.CharField(blank=True, max_length=40, null=True)),
96
+                ('mode', models.CharField(blank=True, max_length=10, null=True)),
97
+                ('cal_mode', models.IntegerField(blank=True, null=True)),
98
+            ],
99
+            options={
100
+                'db_table': 'rotate_broken_test',
101
+                'managed': False,
102
+            },
103
+        ),
104
+    ]

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

@@ -46,6 +46,8 @@
46 46
                         </a>
47 47
                     {% elif field.name == 'file' and obj.file %}
48 48
                     <a href="{{ obj.file.url }}" target="_blank">View</a>
49
+                    {% elif field.name == 'drawing' and obj.drawing %}
50
+                    <a href="{{ obj.drawing.url }}" target="_blank">View</a>
49 51
                     {% elif field.get_internal_type == "DateTimeField" %}
50 52
                         {{ obj|attr:field.name|date:"d/m/Y H:i" }}
51 53
                     {% else %}

+ 30 - 1
app/report/filters.py

@@ -1,5 +1,5 @@
1 1
 import django_filters
2
-from core.models import Report
2
+from core.models import Report, CustomerTemplateMapping, ProductDrawing
3 3
 
4 4
 class ReportFilter(django_filters.FilterSet):
5 5
     name = django_filters.CharFilter(
@@ -26,3 +26,32 @@ class ReportFilter(django_filters.FilterSet):
26 26
     class Meta:
27 27
         model = Report
28 28
         fields = ['name', 'created_by', 'created_at']
29
+
30
+
31
+class CustomerTemplateFilter(django_filters.FilterSet):
32
+    customer_name = django_filters.CharFilter(
33
+        field_name='customer_name',
34
+        lookup_expr='icontains',
35
+        label='Customer Name'
36
+    )
37
+
38
+    template_names = django_filters.CharFilter(
39
+        method='filter_template_names',
40
+        label='Template Name Contains'
41
+    )
42
+
43
+    class Meta:
44
+        model = CustomerTemplateMapping
45
+        fields = ['customer_name', 'template_names']
46
+
47
+    def filter_template_names(self, queryset, name, value):
48
+        return queryset.filter(template_names__icontains=value)
49
+
50
+class ProductDrawingFilter(django_filters.FilterSet):
51
+    code_no = django_filters.CharFilter(lookup_expr='icontains', label='Code No')
52
+    code_no_mks = django_filters.CharFilter(lookup_expr='icontains', label='Code No (MKS)')
53
+    lot_no = django_filters.CharFilter(lookup_expr='icontains', label='Lot No')
54
+
55
+    class Meta:
56
+        model = ProductDrawing
57
+        fields = ['code_no', 'code_no_mks', 'lot_no',]

+ 42 - 0
app/report/templates/report/customer_template_form.html

@@ -0,0 +1,42 @@
1
+{% extends "base.html" %}
2
+{% load legacy_filters %}
3
+{% load tailwind_filters %}
4
+
5
+{% block title %}
6
+    {% if view.title %}
7
+        {{ view.title }}
8
+    {% else %}
9
+        {{ view|class_name }}
10
+    {% endif %}
11
+{% endblock %}
12
+
13
+{% block content %}
14
+<div class="container mx-auto px-4 py-6">
15
+    <h1 class="text-2xl font-bold mb-6">
16
+        {% if view.title %}
17
+            {{ view.title }}
18
+        {% elif view|class_name == "CreateViewClass" %}
19
+            Create {{ model_verbose_name }}
20
+        {% else %}
21
+            Update {{ model_verbose_name }}
22
+        {% endif %}
23
+    </h1>
24
+
25
+    <!-- Render the Form -->
26
+    <form method="post" enctype="multipart/form-data" >
27
+        {% csrf_token %}
28
+        <div>
29
+          
30
+        {{ form|crispy }}
31
+        </div>
32
+        <div class='mt-4'>
33
+            <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
34
+                Save
35
+            </button>
36
+            <a href="{% url list_url_name %}" class="bg-gray-300 text-gray-800 px-4 py-2 rounded hover:bg-gray-400">
37
+                Cancel
38
+            </a>
39
+        </div>
40
+    </form>
41
+</div>
42
+{% endblock %}

+ 126 - 0
app/report/templates/report/customer_template_list.html

@@ -0,0 +1,126 @@
1
+{% extends "base.html" %}
2
+{% load legacy_filters %}
3
+{% load tailwind_filters %}
4
+{% block title %}{{ page_title }}{% endblock %}
5
+
6
+{% block content %}
7
+<div class="container mx-auto px-4 py-6">
8
+    <h1 class="text-3xl font-bold text-gray-800 mb-4">{{ page_title }}</h1>
9
+
10
+    <!-- Filter Form -->
11
+    <form method="get" class="flex items-center space-x-4 mb-4">
12
+        {{ filter.form | crispy }}
13
+        <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Filter</button>
14
+        <a href="?" class="bg-gray-300 text-gray-800 px-4 py-2 rounded hover:bg-gray-400">Reset</a>
15
+    </form>
16
+
17
+    <!-- Create Button -->
18
+    <div class="my-4 flex">
19
+        <p class="text-gray-600 mb-4 mr-auto">
20
+            Total Records: {{ page_obj.paginator.count }}
21
+        </p>
22
+        <a href="{% url create_url %}" class="ml-auto bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
23
+            Create New Data
24
+        </a>
25
+    </div>
26
+
27
+    <!-- Data Table -->
28
+    <div class="clear-both bg-white shadow rounded-lg overflow-x-auto">
29
+<table class="w-full border-collapse border border-gray-200">
30
+    <thead>
31
+        <tr class="bg-gray-100 text-left text-sm uppercase">
32
+            {% for field in fields %}
33
+            <th class="border border-gray-200 px-4 py-2 text-left">{% firstof field.verbose_name field.name %} </th>
34
+            {% endfor %}
35
+            <th class="py-2 px-4 border-b">Actions</th>
36
+        </tr>
37
+    </thead>
38
+    <tbody>
39
+        {% for obj in page_obj %}
40
+            <tr class="hover:bg-gray-50">
41
+                {% for field in fields %}
42
+                <td class="border border-gray-200 px-4 py-2">
43
+                    {% if field.name == 'id' %}
44
+                        <a href="{% url update_url obj.pk %}" class="text-blue-500 hover:underline">
45
+                            {{ obj|attr:field.name }}
46
+                        </a>
47
+                    {% elif field.name == 'template_names' %}
48
+
49
+                      {% with template_list=obj|attr:field.name %}
50
+                          {% for t in template_list %}
51
+                              {{ sheet_names | get_item:t }}
52
+                              {% if not forloop.last %}, {% endif %}
53
+                          {% endfor %}
54
+                      {% endwith %}
55
+                    {% elif field.name == 'file' and obj.file %}
56
+                    <a href="{{ obj.file.url }}" target="_blank">View</a>
57
+                    {% elif field.get_internal_type == "DateTimeField" %}
58
+                        {{ obj|attr:field.name|date:"d/m/Y H:i" }}
59
+                    {% else %}
60
+                        {{ obj|attr:field.name | safe_floatformat:2 }}
61
+                    {% endif %}
62
+                </td>
63
+                {% endfor %}
64
+                <td class="py-2 px-4 border-b">
65
+                    <a href="{% url update_url obj.pk %}" 
66
+                       class="bg-blue-500 text-white px-3 py-2 rounded hover:bg-blue-600">Edit</a>
67
+                    <a href="{% url delete_url obj.pk %}" 
68
+                       class="bg-red-500 text-white px-3 py-2 rounded hover:bg-red-600">Delete</a>
69
+                </td>
70
+            </tr>
71
+        {% empty %}
72
+            <tr>
73
+                <td colspan="5" class="py-4 px-4 text-center text-gray-600">No data available.</td>
74
+            </tr>
75
+        {% endfor %}
76
+    </tbody>
77
+</table>
78
+    </div>
79
+
80
+    <!-- Pagination -->
81
+    <div class="mt-6 flex justify-between items-center">
82
+        <div>
83
+            <span class="text-sm text-gray-600">
84
+                Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
85
+            </span>
86
+        </div>
87
+        <div class="space-x-2">
88
+            {% if page_obj.has_previous %}
89
+                <a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}page=1" 
90
+                   class="text-blue-500 hover:underline">First</a>
91
+                <a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}page={{ page_obj.previous_page_number }}" 
92
+                   class="text-blue-500 hover:underline">Previous</a>
93
+            {% endif %}
94
+            {% for page_num in page_obj.paginator.page_range %}
95
+                {% if page_num == page_obj.number %}
96
+                    <span class="font-bold text-gray-700">{{ page_num }}</span>
97
+                {% elif page_num == 1 or page_num == page_obj.paginator.num_pages or page_num >= page_obj.number|add:"-2" and page_num <= page_obj.number|add:"2" %}
98
+                    <a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}page={{ page_num }}" 
99
+                       class="text-blue-500 hover:underline">{{ page_num }}</a>
100
+                {% elif page_num == page_obj.number|add:-3 or page_num == page_obj.number|add:3 %}
101
+                    <span class="mx-1">...</span>
102
+                {% endif %}
103
+            {% endfor %}
104
+            {% if page_obj.has_next %}
105
+                <a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}page={{ page_obj.next_page_number }}" 
106
+                   class="text-blue-500 hover:underline">Next</a>
107
+                <a href="?{% for key, value in request.GET.items %}{% if key != 'page' %}{{ key }}={{ value|urlencode }}&{% endif %}{% endfor %}page={{ page_obj.paginator.num_pages }}" 
108
+                   class="text-blue-500 hover:underline">Last</a>
109
+            {% endif %}
110
+        </div>
111
+    </div>
112
+</div>
113
+<style>
114
+/* Remove fixed layout and allow columns to grow dynamically */
115
+table {
116
+    table-layout: auto; /* Default is auto, can be explicitly set */
117
+    width: 100%; /* Ensures table spans available space */
118
+}
119
+
120
+th, td {
121
+    white-space: nowrap; /* Prevents text wrapping */
122
+    text-overflow: ellipsis; /* Adds ellipsis for overflowed content if combined with max-width */
123
+    vertical-align: top; /* Aligns content to the top */
124
+}
125
+</style>
126
+{% endblock %}

+ 12 - 1
app/report/urls.py

@@ -1,8 +1,10 @@
1 1
 from django.urls import path
2 2
 from . import views
3
-from .views import ReportCRUDView
3
+from .views import ReportCRUDView, CustomerTemplateCRUDView, ProductDrawingCRUDView
4 4
 
5 5
 report_crud = ReportCRUDView()
6
+customer_templates_crud = CustomerTemplateCRUDView()
7
+product_drawings_crud = ProductDrawingCRUDView()
6 8
 
7 9
 app_name = "report"  # Use this namespace for reverse URL lookups
8 10
 
@@ -16,6 +18,15 @@ urlpatterns = [
16 18
     path('coi/', views.coi_view, name='coi-view'),
17 19
     path('report/generate/', views.gen_report_view, name='gen_report'),
18 20
 
21
+    path('customer_templates/', customer_templates_crud.get_list_view().as_view(), name='customer_templates-list'),
22
+    path('customer_templates/create/', customer_templates_crud.get_create_view().as_view(), name='customer_templates-create'),
23
+    path('customer_templates/<str:pk>/update/', customer_templates_crud.get_update_view().as_view(), name='customer_templates-update'),
24
+    path('customer_templates/<str:pk>/delete/', customer_templates_crud.get_delete_view().as_view(), name='customer_templates-delete'),
25
+    
26
+    path('product_drawings/', product_drawings_crud.get_list_view().as_view(), name='product_drawings-list'),
27
+    path('product_drawings/create/', product_drawings_crud.get_create_view().as_view(), name='product_drawings-create'),
28
+    path('product_drawings/<str:pk>/update/', product_drawings_crud.get_update_view().as_view(), name='product_drawings-update'),
29
+    path('product_drawings/<str:pk>/delete/', product_drawings_crud.get_delete_view().as_view(), name='product_drawings-delete'),
19 30
     # path('create/', views.create_report, name='create'),  # Create a new report
20 31
     # path('<int:pk>/', views.detail_report, name='detail'),  # View details of a specific report
21 32
     # path('<int:pk>/update/', views.update_report, name='update'),  # Update a specific report

+ 84 - 16
app/report/views.py

@@ -1,10 +1,11 @@
1 1
 from django.shortcuts import render, redirect, get_object_or_404
2 2
 from django.core.paginator import Paginator
3 3
 from django.contrib import messages
4
-from core.models import Report, AllProductDimensionForInsProcess
5
-from core.forms import ReportForm
6
-from core.utils import ConfigurableCRUDView, queryFromMaster
7
-from .filters import ReportFilter
4
+from core.models import Report, AllProductDimensionForInsProcess, CustomerTemplateMapping, \
5
+                ProductDrawing
6
+from core.forms import ReportForm, CustomerTemplateMappingForm, ProductDrawingForm
7
+from core.utils import ConfigurableCRUDView, queryFromMaster, SHEET_NAMES
8
+from .filters import ReportFilter, CustomerTemplateFilter, ProductDrawingFilter
8 9
 from .forms import ExportOptionsForm
9 10
 from pprint import pprint
10 11
 
@@ -24,6 +25,10 @@ from django.conf import settings
24 25
 
25 26
 from itertools import chain
26 27
 
28
+from django_filters.views import FilterView
29
+
30
+from django.views.generic import (
31
+    ListView,)
27 32
 
28 33
 def index(request):
29 34
     reports = Report.objects.all()
@@ -886,18 +891,6 @@ def create_coi_file(lot_no, sheets, user, md):
886 891
     pprint(f"outputfile = {output_file}")
887 892
     return report
888 893
 
889
-SHEET_NAMES = {
890
-    'hardness_out': 'Hardness Out',
891
-    'hardness_out_in': 'Hardness Out/In', 
892
-    'hardness_both_size': 'Hardness Both Size',
893
-    'dimension': 'Dimension',
894
-    'dimension_app': 'Dimension Appearance',
895
-    'dimension_bal_weight': 'Dimension Balance/Weight',
896
-    'dim_bal_app_hard': 'Dimension Balance/Appearance/Hardness',
897
-    'dim_bal_app_rot_hard': 'Dimension Balance/Appearance/Rotation/Hardness',
898
-    'thickness_8_point': 'Thickness 8 Points',
899
-    'centering': 'Centering',
900
-}
901 894
 def get_fields(model):
902 895
     # model_fields = {f.name: f for f in model._meta.get_fields()}
903 896
     # fields = list(model_fields.values())
@@ -1051,3 +1044,78 @@ def gen_report_view(request):
1051 1044
 
1052 1045
 
1053 1046
 
1047
+class CustomerTemplateCRUDView(ConfigurableCRUDView):
1048
+    model = CustomerTemplateMapping
1049
+    list_template_name = 'report/customer_template_list.html'
1050
+    detail_template_name = 'legacy/datacrud_detail.html'
1051
+    form_template_name = 'report/customer_template_form.html'
1052
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
1053
+    filterset_class = CustomerTemplateFilter
1054
+
1055
+    page_title = "Customer Template Mapping"
1056
+
1057
+    # URL name mappings
1058
+    list_url_name = 'report:customer_templates-list'
1059
+    create_url_name = 'report:customer_templates-create'
1060
+    update_url_name = 'report:customer_templates-update'
1061
+    delete_url_name = 'report:customer_templates-delete'
1062
+    config_fields = ["id", "customer_name", "template_names", "created_at"] 
1063
+    config_field_orders = ["id", "customer_name", "template_names", "created_at",  "created_by"]
1064
+    # config_readonly_fields = ["lot_no"]
1065
+    config_edit_fields = None
1066
+    ordering = ["-created_at", "-id",]
1067
+    form_class = CustomerTemplateMappingForm
1068
+
1069
+
1070
+    def get_list_view(self):
1071
+        class ListViewClass(FilterView, ListView):
1072
+            model = self.model
1073
+            template_name = self.list_template_name
1074
+            paginate_by = self.paginate_by
1075
+            filterset_class = self.filterset_class
1076
+            ordering = self.ordering
1077
+
1078
+            def get_context_data(inner_self, **kwargs):
1079
+                context = super().get_context_data(**kwargs)
1080
+                fields = self.get_fields()
1081
+                context.update({
1082
+                    'fields': [f for f in fields],
1083
+                    'sheet_names': SHEET_NAMES,
1084
+
1085
+                    # 'fields': [field for field in self.model._meta.get_fields()],
1086
+                    'page_title': self.page_title,
1087
+                    'list_url': self.list_url_name,
1088
+                    'create_url': self.create_url_name,
1089
+                    'update_url': self.update_url_name,
1090
+                    'delete_url': self.delete_url_name,
1091
+                    'bs': self.get_breadcrumbs('list'),
1092
+                })
1093
+                return context
1094
+
1095
+        return ListViewClass
1096
+
1097
+
1098
+
1099
+class ProductDrawingCRUDView(ConfigurableCRUDView):
1100
+    model = ProductDrawing
1101
+    list_template_name = 'legacy/datacrud_list.html'
1102
+    detail_template_name = 'legacy/datacrud_detail.html'
1103
+    form_template_name = 'legacy/datacrud_form.html'
1104
+    confirm_delete_template_name = 'legacy/datacrud_confirm_delete.html'
1105
+    filterset_class = ProductDrawingFilter
1106
+
1107
+    page_title = "Product Drawing"
1108
+
1109
+    # URL name mappings
1110
+    list_url_name = 'report:product_drawings-list'
1111
+    create_url_name = 'report:product_drawings-create'
1112
+    update_url_name = 'report:product_drawings-update'
1113
+    delete_url_name = 'report:product_drawings-delete'
1114
+    config_fields = ["id", "code_no", "code_no_mks", "lot_no", "drawing", "description", "created_at"] 
1115
+    #config_field_orders = ["id", "customer_name", "template_names", "created_at",  "created_by"]
1116
+    # config_readonly_fields = ["lot_no"]
1117
+    config_edit_fields = None
1118
+    ordering = ["-created_at", "-id",]
1119
+    form_class = ProductDrawingForm
1120
+
1121
+

binární
app/src.zip


+ 18 - 0
app/sysadmin/migrations/0002_alter_userprofile_position.py

@@ -0,0 +1,18 @@
1
+# Generated by Django 4.2 on 2025-05-06 04:36
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('sysadmin', '0001_initial'),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AlterField(
14
+            model_name='userprofile',
15
+            name='position',
16
+            field=models.CharField(blank=True, choices=[('QA_STAFF', 'QA Staff'), ('QA_MANAGER', 'QA. MG.'), ('QA_AST_MANAGER', 'QA. Asst. MG.'), ('QA_ENGINEER', 'QA. Engineer')], max_length=20, null=True),
17
+        ),
18
+    ]

+ 2 - 0
app/templates/base.html

@@ -66,6 +66,8 @@
66 66
                 <li><a href="/dashboard/" 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">Dashboard</span></a></li>
67 67
                 <li><a href="{% url "report:coi-view" %}" 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">COI</span></a></li>
68 68
                 <li><a href="{% url "report:report-list" %}" 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">Reports</span></a></li>
69
+                <li><a href="{% url "report:customer_templates-list" %}" 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">Customer Templates</span></a></li>
70
+                <li><a href="{% url "report:product_drawings-list" %}" 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">Drawing</span></a></li>
69 71
                  <li>
70 72
                     <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="dropdown-example" data-collapse-toggle="dropdown-example">
71 73
                           <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">

tum/whitesports - Gogs: Simplico Git Service

Ei kuvausta

class-wp-plugins-list-table.php 42KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238
  1. <?php
  2. /**
  3. * List Table API: WP_Plugins_List_Table class
  4. *
  5. * @package WordPress
  6. * @subpackage Administration
  7. * @since 3.1.0
  8. */
  9. /**
  10. * Core class used to implement displaying installed plugins in a list table.
  11. *
  12. * @since 3.1.0
  13. * @access private
  14. *
  15. * @see WP_List_Table
  16. */
  17. class WP_Plugins_List_Table extends WP_List_Table {
  18. /**
  19. * Whether to show the auto-updates UI.
  20. *
  21. * @since 5.5.0
  22. *
  23. * @var bool True if auto-updates UI is to be shown, false otherwise.
  24. */
  25. protected $show_autoupdates = true;
  26. /**
  27. * Constructor.
  28. *
  29. * @since 3.1.0
  30. *
  31. * @see WP_List_Table::__construct() for more information on default arguments.
  32. *
  33. * @global string $status
  34. * @global int $page
  35. *
  36. * @param array $args An associative array of arguments.
  37. */
  38. public function __construct( $args = array() ) {
  39. global $status, $page;
  40. parent::__construct(
  41. array(
  42. 'plural' => 'plugins',
  43. 'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
  44. )
  45. );
  46. $allowed_statuses = array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused', 'auto-update-enabled', 'auto-update-disabled' );
  47. $status = 'all';
  48. if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], $allowed_statuses, true ) ) {
  49. $status = $_REQUEST['plugin_status'];
  50. }
  51. if ( isset( $_REQUEST['s'] ) ) {
  52. $_SERVER['REQUEST_URI'] = add_query_arg( 's', wp_unslash( $_REQUEST['s'] ) );
  53. }
  54. $page = $this->get_pagenum();
  55. $this->show_autoupdates = wp_is_auto_update_enabled_for_type( 'plugin' )
  56. && current_user_can( 'update_plugins' )
  57. && ( ! is_multisite() || $this->screen->in_admin( 'network' ) )
  58. && ! in_array( $status, array( 'mustuse', 'dropins' ), true );
  59. }
  60. /**
  61. * @return array
  62. */
  63. protected function get_table_classes() {
  64. return array( 'widefat', $this->_args['plural'] );
  65. }
  66. /**
  67. * @return bool
  68. */
  69. public function ajax_user_can() {
  70. return current_user_can( 'activate_plugins' );
  71. }
  72. /**
  73. * @global string $status
  74. * @global array $plugins
  75. * @global array $totals
  76. * @global int $page
  77. * @global string $orderby
  78. * @global string $order
  79. * @global string $s
  80. */
  81. public function prepare_items() {
  82. global $status, $plugins, $totals, $page, $orderby, $order, $s;
  83. wp_reset_vars( array( 'orderby', 'order' ) );
  84. /**
  85. * Filters the full array of plugins to list in the Plugins list table.
  86. *
  87. * @since 3.0.0
  88. *
  89. * @see get_plugins()
  90. *
  91. * @param array $all_plugins An array of plugins to display in the list table.
  92. */
  93. $all_plugins = apply_filters( 'all_plugins', get_plugins() );
  94. $plugins = array(
  95. 'all' => $all_plugins,
  96. 'search' => array(),
  97. 'active' => array(),
  98. 'inactive' => array(),
  99. 'recently_activated' => array(),
  100. 'upgrade' => array(),
  101. 'mustuse' => array(),
  102. 'dropins' => array(),
  103. 'paused' => array(),
  104. );
  105. if ( $this->show_autoupdates ) {
  106. $auto_updates = (array) get_site_option( 'auto_update_plugins', array() );
  107. $plugins['auto-update-enabled'] = array();
  108. $plugins['auto-update-disabled'] = array();
  109. }
  110. $screen = $this->screen;
  111. if ( ! is_multisite() || ( $screen->in_admin( 'network' ) && current_user_can( 'manage_network_plugins' ) ) ) {
  112. /**
  113. * Filters whether to display the advanced plugins list table.
  114. *
  115. * There are two types of advanced plugins - must-use and drop-ins -
  116. * which can be used in a single site or Multisite network.
  117. *
  118. * The $type parameter allows you to differentiate between the type of advanced
  119. * plugins to filter the display of. Contexts include 'mustuse' and 'dropins'.
  120. *
  121. * @since 3.0.0
  122. *
  123. * @param bool $show Whether to show the advanced plugins for the specified
  124. * plugin type. Default true.
  125. * @param string $type The plugin type. Accepts 'mustuse', 'dropins'.
  126. */
  127. if ( apply_filters( 'show_advanced_plugins', true, 'mustuse' ) ) {
  128. $plugins['mustuse'] = get_mu_plugins();
  129. }
  130. /** This action is documented in wp-admin/includes/class-wp-plugins-list-table.php */
  131. if ( apply_filters( 'show_advanced_plugins', true, 'dropins' ) ) {
  132. $plugins['dropins'] = get_dropins();
  133. }
  134. if ( current_user_can( 'update_plugins' ) ) {
  135. $current = get_site_transient( 'update_plugins' );
  136. foreach ( (array) $plugins['all'] as $plugin_file => $plugin_data ) {
  137. if ( isset( $current->response[ $plugin_file ] ) ) {
  138. $plugins['all'][ $plugin_file ]['update'] = true;
  139. $plugins['upgrade'][ $plugin_file ] = $plugins['all'][ $plugin_file ];
  140. }
  141. }
  142. }
  143. }
  144. if ( ! $screen->in_admin( 'network' ) ) {
  145. $show = current_user_can( 'manage_network_plugins' );
  146. /**
  147. * Filters whether to display network-active plugins alongside plugins active for the current site.
  148. *
  149. * This also controls the display of inactive network-only plugins (plugins with
  150. * "Network: true" in the plugin header).
  151. *
  152. * Plugins cannot be network-activated or network-deactivated from this screen.
  153. *
  154. * @since 4.4.0
  155. *
  156. * @param bool $show Whether to show network-active plugins. Default is whether the current
  157. * user can manage network plugins (ie. a Super Admin).
  158. */
  159. $show_network_active = apply_filters( 'show_network_active_plugins', $show );
  160. }
  161. if ( $screen->in_admin( 'network' ) ) {
  162. $recently_activated = get_site_option( 'recently_activated', array() );
  163. } else {
  164. $recently_activated = get_option( 'recently_activated', array() );
  165. }
  166. foreach ( $recently_activated as $key => $time ) {
  167. if ( $time + WEEK_IN_SECONDS < time() ) {
  168. unset( $recently_activated[ $key ] );
  169. }
  170. }
  171. if ( $screen->in_admin( 'network' ) ) {
  172. update_site_option( 'recently_activated', $recently_activated );
  173. } else {
  174. update_option( 'recently_activated', $recently_activated );
  175. }
  176. $plugin_info = get_site_transient( 'update_plugins' );
  177. foreach ( (array) $plugins['all'] as $plugin_file => $plugin_data ) {
  178. // Extra info if known. array_merge() ensures $plugin_data has precedence if keys collide.
  179. if ( isset( $plugin_info->response[ $plugin_file ] ) ) {
  180. $plugin_data = array_merge( (array) $plugin_info->response[ $plugin_file ], array( 'update-supported' => true ), $plugin_data );
  181. } elseif ( isset( $plugin_info->no_update[ $plugin_file ] ) ) {
  182. $plugin_data = array_merge( (array) $plugin_info->no_update[ $plugin_file ], array( 'update-supported' => true ), $plugin_data );
  183. } elseif ( empty( $plugin_data['update-supported'] ) ) {
  184. $plugin_data['update-supported'] = false;
  185. }
  186. /*
  187. * Create the payload that's used for the auto_update_plugin filter.
  188. * This is the same data contained within $plugin_info->(response|no_update) however
  189. * not all plugins will be contained in those keys, this avoids unexpected warnings.
  190. */
  191. $filter_payload = array(
  192. 'id' => $plugin_file,
  193. 'slug' => '',
  194. 'plugin' => $plugin_file,
  195. 'new_version' => '',
  196. 'url' => '',
  197. 'package' => '',
  198. 'icons' => array(),
  199. 'banners' => array(),
  200. 'banners_rtl' => array(),
  201. 'tested' => '',
  202. 'requires_php' => '',
  203. 'compatibility' => new stdClass(),
  204. );
  205. $filter_payload = (object) wp_parse_args( $plugin_data, $filter_payload );
  206. $auto_update_forced = wp_is_auto_update_forced_for_item( 'plugin', null, $filter_payload );
  207. if ( ! is_null( $auto_update_forced ) ) {
  208. $plugin_data['auto-update-forced'] = $auto_update_forced;
  209. }
  210. $plugins['all'][ $plugin_file ] = $plugin_data;
  211. // Make sure that $plugins['upgrade'] also receives the extra info since it is used on ?plugin_status=upgrade.
  212. if ( isset( $plugins['upgrade'][ $plugin_file ] ) ) {
  213. $plugins['upgrade'][ $plugin_file ] = $plugin_data;
  214. }
  215. // Filter into individual sections.
  216. if ( is_multisite() && ! $screen->in_admin( 'network' ) && is_network_only_plugin( $plugin_file ) && ! is_plugin_active( $plugin_file ) ) {
  217. if ( $show_network_active ) {
  218. // On the non-network screen, show inactive network-only plugins if allowed.
  219. $plugins['inactive'][ $plugin_file ] = $plugin_data;
  220. } else {
  221. // On the non-network screen, filter out network-only plugins as long as they're not individually active.
  222. unset( $plugins['all'][ $plugin_file ] );
  223. }
  224. } elseif ( ! $screen->in_admin( 'network' ) && is_plugin_active_for_network( $plugin_file ) ) {
  225. if ( $show_network_active ) {
  226. // On the non-network screen, show network-active plugins if allowed.
  227. $plugins['active'][ $plugin_file ] = $plugin_data;
  228. } else {
  229. // On the non-network screen, filter out network-active plugins.
  230. unset( $plugins['all'][ $plugin_file ] );
  231. }
  232. } elseif ( ( ! $screen->in_admin( 'network' ) && is_plugin_active( $plugin_file ) )
  233. || ( $screen->in_admin( 'network' ) && is_plugin_active_for_network( $plugin_file ) ) ) {
  234. // On the non-network screen, populate the active list with plugins that are individually activated.
  235. // On the network admin screen, populate the active list with plugins that are network-activated.
  236. $plugins['active'][ $plugin_file ] = $plugin_data;
  237. if ( ! $screen->in_admin( 'network' ) && is_plugin_paused( $plugin_file ) ) {
  238. $plugins['paused'][ $plugin_file ] = $plugin_data;
  239. }
  240. } else {
  241. if ( isset( $recently_activated[ $plugin_file ] ) ) {
  242. // Populate the recently activated list with plugins that have been recently activated.
  243. $plugins['recently_activated'][ $plugin_file ] = $plugin_data;
  244. }
  245. // Populate the inactive list with plugins that aren't activated.
  246. $plugins['inactive'][ $plugin_file ] = $plugin_data;
  247. }
  248. if ( $this->show_autoupdates ) {
  249. $enabled = in_array( $plugin_file, $auto_updates, true ) && $plugin_data['update-supported'];
  250. if ( isset( $plugin_data['auto-update-forced'] ) ) {
  251. $enabled = (bool) $plugin_data['auto-update-forced'];
  252. }
  253. if ( $enabled ) {
  254. $plugins['auto-update-enabled'][ $plugin_file ] = $plugin_data;
  255. } else {
  256. $plugins['auto-update-disabled'][ $plugin_file ] = $plugin_data;
  257. }
  258. }
  259. }
  260. if ( strlen( $s ) ) {
  261. $status = 'search';
  262. $plugins['search'] = array_filter( $plugins['all'], array( $this, '_search_callback' ) );
  263. }
  264. $totals = array();
  265. foreach ( $plugins as $type => $list ) {
  266. $totals[ $type ] = count( $list );
  267. }
  268. if ( empty( $plugins[ $status ] ) && ! in_array( $status, array( 'all', 'search' ), true ) ) {
  269. $status = 'all';
  270. }
  271. $this->items = array();
  272. foreach ( $plugins[ $status ] as $plugin_file => $plugin_data ) {
  273. // Translate, don't apply markup, sanitize HTML.
  274. $this->items[ $plugin_file ] = _get_plugin_data_markup_translate( $plugin_file, $plugin_data, false, true );
  275. }
  276. $total_this_page = $totals[ $status ];
  277. $js_plugins = array();
  278. foreach ( $plugins as $key => $list ) {
  279. $js_plugins[ $key ] = array_keys( $list );
  280. }
  281. wp_localize_script(
  282. 'updates',
  283. '_wpUpdatesItemCounts',
  284. array(
  285. 'plugins' => $js_plugins,
  286. 'totals' => wp_get_update_data(),
  287. )
  288. );
  289. if ( ! $orderby ) {
  290. $orderby = 'Name';
  291. } else {
  292. $orderby = ucfirst( $orderby );
  293. }
  294. $order = strtoupper( $order );
  295. uasort( $this->items, array( $this, '_order_callback' ) );
  296. $plugins_per_page = $this->get_items_per_page( str_replace( '-', '_', $screen->id . '_per_page' ), 999 );
  297. $start = ( $page - 1 ) * $plugins_per_page;
  298. if ( $total_this_page > $plugins_per_page ) {
  299. $this->items = array_slice( $this->items, $start, $plugins_per_page );
  300. }
  301. $this->set_pagination_args(
  302. array(
  303. 'total_items' => $total_this_page,
  304. 'per_page' => $plugins_per_page,
  305. )
  306. );
  307. }
  308. /**
  309. * @global string $s URL encoded search term.
  310. *
  311. * @param array $plugin
  312. * @return bool
  313. */
  314. public function _search_callback( $plugin ) {
  315. global $s;
  316. foreach ( $plugin as $value ) {
  317. if ( is_string( $value ) && false !== stripos( strip_tags( $value ), urldecode( $s ) ) ) {
  318. return true;
  319. }
  320. }
  321. return false;
  322. }
  323. /**
  324. * @global string $orderby
  325. * @global string $order
  326. * @param array $plugin_a
  327. * @param array $plugin_b
  328. * @return int
  329. */
  330. public function _order_callback( $plugin_a, $plugin_b ) {
  331. global $orderby, $order;
  332. $a = $plugin_a[ $orderby ];
  333. $b = $plugin_b[ $orderby ];
  334. if ( $a === $b ) {
  335. return 0;
  336. }
  337. if ( 'DESC' === $order ) {
  338. return strcasecmp( $b, $a );
  339. } else {
  340. return strcasecmp( $a, $b );
  341. }
  342. }
  343. /**
  344. * @global array $plugins
  345. */
  346. public function no_items() {
  347. global $plugins;
  348. if ( ! empty( $_REQUEST['s'] ) ) {
  349. $s = esc_html( wp_unslash( $_REQUEST['s'] ) );
  350. /* translators: %s: Plugin search term. */
  351. printf( __( 'No plugins found for: %s.' ), '<strong>' . $s . '</strong>' );
  352. // We assume that somebody who can install plugins in multisite is experienced enough to not need this helper link.
  353. if ( ! is_multisite() && current_user_can( 'install_plugins' ) ) {
  354. echo ' <a href="' . esc_url( admin_url( 'plugin-install.php?tab=search&s=' . urlencode( $s ) ) ) . '">' . __( 'Search for plugins in the WordPress Plugin Directory.' ) . '</a>';
  355. }
  356. } elseif ( ! empty( $plugins['all'] ) ) {
  357. _e( 'No plugins found.' );
  358. } else {
  359. _e( 'No plugins are currently available.' );
  360. }
  361. }
  362. /**
  363. * Displays the search box.
  364. *
  365. * @since 4.6.0
  366. *
  367. * @param string $text The 'submit' button label.
  368. * @param string $input_id ID attribute value for the search input field.
  369. */
  370. public function search_box( $text, $input_id ) {
  371. if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
  372. return;
  373. }
  374. $input_id = $input_id . '-search-input';
  375. if ( ! empty( $_REQUEST['orderby'] ) ) {
  376. echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
  377. }
  378. if ( ! empty( $_REQUEST['order'] ) ) {
  379. echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
  380. }
  381. ?>
  382. <p class="search-box">
  383. <label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label>
  384. <input type="search" id="<?php echo esc_attr( $input_id ); ?>" class="wp-filter-search" name="s" value="<?php _admin_search_query(); ?>" placeholder="<?php esc_attr_e( 'Search installed plugins...' ); ?>" />
  385. <?php submit_button( $text, 'hide-if-js', '', false, array( 'id' => 'search-submit' ) ); ?>
  386. </p>
  387. <?php
  388. }
  389. /**
  390. * @global string $status
  391. * @return array
  392. */
  393. public function get_columns() {
  394. global $status;
  395. $columns = array(
  396. 'cb' => ! in_array( $status, array( 'mustuse', 'dropins' ), true ) ? '<input type="checkbox" />' : '',
  397. 'name' => __( 'Plugin' ),
  398. 'description' => __( 'Description' ),
  399. );
  400. if ( $this->show_autoupdates ) {
  401. $columns['auto-updates'] = __( 'Automatic Updates' );
  402. }
  403. return $columns;
  404. }
  405. /**
  406. * @return array
  407. */
  408. protected function get_sortable_columns() {
  409. return array();
  410. }
  411. /**
  412. * @global array $totals
  413. * @global string $status
  414. * @return array
  415. */
  416. protected function get_views() {
  417. global $totals, $status;
  418. $status_links = array();
  419. foreach ( $totals as $type => $count ) {
  420. if ( ! $count ) {
  421. continue;
  422. }
  423. switch ( $type ) {
  424. case 'all':
  425. /* translators: %s: Number of plugins. */
  426. $text = _nx(
  427. 'All <span class="count">(%s)</span>',
  428. 'All <span class="count">(%s)</span>',
  429. $count,
  430. 'plugins'
  431. );
  432. break;
  433. case 'active':
  434. /* translators: %s: Number of plugins. */
  435. $text = _n(
  436. 'Active <span class="count">(%s)</span>',
  437. 'Active <span class="count">(%s)</span>',
  438. $count
  439. );
  440. break;
  441. case 'recently_activated':
  442. /* translators: %s: Number of plugins. */
  443. $text = _n(
  444. 'Recently Active <span class="count">(%s)</span>',
  445. 'Recently Active <span class="count">(%s)</span>',
  446. $count
  447. );
  448. break;
  449. case 'inactive':
  450. /* translators: %s: Number of plugins. */
  451. $text = _n(
  452. 'Inactive <span class="count">(%s)</span>',
  453. 'Inactive <span class="count">(%s)</span>',
  454. $count
  455. );
  456. break;
  457. case 'mustuse':
  458. /* translators: %s: Number of plugins. */
  459. $text = _n(
  460. 'Must-Use <span class="count">(%s)</span>',
  461. 'Must-Use <span class="count">(%s)</span>',
  462. $count
  463. );
  464. break;
  465. case 'dropins':
  466. /* translators: %s: Number of plugins. */
  467. $text = _n(
  468. 'Drop-in <span class="count">(%s)</span>',
  469. 'Drop-ins <span class="count">(%s)</span>',
  470. $count
  471. );
  472. break;
  473. case 'paused':
  474. /* translators: %s: Number of plugins. */
  475. $text = _n(
  476. 'Paused <span class="count">(%s)</span>',
  477. 'Paused <span class="count">(%s)</span>',
  478. $count
  479. );
  480. break;
  481. case 'upgrade':
  482. /* translators: %s: Number of plugins. */
  483. $text = _n(
  484. 'Update Available <span class="count">(%s)</span>',
  485. 'Update Available <span class="count">(%s)</span>',
  486. $count
  487. );
  488. break;
  489. case 'auto-update-enabled':
  490. /* translators: %s: Number of plugins. */
  491. $text = _n(
  492. 'Auto-updates Enabled <span class="count">(%s)</span>',
  493. 'Auto-updates Enabled <span class="count">(%s)</span>',
  494. $count
  495. );
  496. break;
  497. case 'auto-update-disabled':
  498. /* translators: %s: Number of plugins. */
  499. $text = _n(
  500. 'Auto-updates Disabled <span class="count">(%s)</span>',
  501. 'Auto-updates Disabled <span class="count">(%s)</span>',
  502. $count
  503. );
  504. break;
  505. }
  506. if ( 'search' !== $type ) {
  507. $status_links[ $type ] = sprintf(
  508. "<a href='%s'%s>%s</a>",
  509. add_query_arg( 'plugin_status', $type, 'plugins.php' ),
  510. ( $type === $status ) ? ' class="current" aria-current="page"' : '',
  511. sprintf( $text, number_format_i18n( $count ) )
  512. );
  513. }
  514. }
  515. return $status_links;
  516. }
  517. /**
  518. * @global string $status
  519. * @return array
  520. */
  521. protected function get_bulk_actions() {
  522. global $status;
  523. $actions = array();
  524. if ( 'active' !== $status ) {
  525. $actions['activate-selected'] = $this->screen->in_admin( 'network' ) ? __( 'Network Activate' ) : __( 'Activate' );
  526. }
  527. if ( 'inactive' !== $status && 'recent' !== $status ) {
  528. $actions['deactivate-selected'] = $this->screen->in_admin( 'network' ) ? __( 'Network Deactivate' ) : __( 'Deactivate' );
  529. }
  530. if ( ! is_multisite() || $this->screen->in_admin( 'network' ) ) {
  531. if ( current_user_can( 'update_plugins' ) ) {
  532. $actions['update-selected'] = __( 'Update' );
  533. }
  534. if ( current_user_can( 'delete_plugins' ) && ( 'active' !== $status ) ) {
  535. $actions['delete-selected'] = __( 'Delete' );
  536. }
  537. if ( $this->show_autoupdates ) {
  538. if ( 'auto-update-enabled' !== $status ) {
  539. $actions['enable-auto-update-selected'] = __( 'Enable Auto-updates' );
  540. }
  541. if ( 'auto-update-disabled' !== $status ) {
  542. $actions['disable-auto-update-selected'] = __( 'Disable Auto-updates' );
  543. }
  544. }
  545. }
  546. return $actions;
  547. }
  548. /**
  549. * @global string $status
  550. * @param string $which
  551. */
  552. public function bulk_actions( $which = '' ) {
  553. global $status;
  554. if ( in_array( $status, array( 'mustuse', 'dropins' ), true ) ) {
  555. return;
  556. }
  557. parent::bulk_actions( $which );
  558. }
  559. /**
  560. * @global string $status
  561. * @param string $which
  562. */
  563. protected function extra_tablenav( $which ) {
  564. global $status;
  565. if ( ! in_array( $status, array( 'recently_activated', 'mustuse', 'dropins' ), true ) ) {
  566. return;
  567. }
  568. echo '<div class="alignleft actions">';
  569. if ( 'recently_activated' === $status ) {
  570. submit_button( __( 'Clear List' ), '', 'clear-recent-list', false );
  571. } elseif ( 'top' === $which && 'mustuse' === $status ) {
  572. echo '<p>' . sprintf(
  573. /* translators: %s: mu-plugins directory name. */
  574. __( 'Files in the %s directory are executed automatically.' ),
  575. '<code>' . str_replace( ABSPATH, '/', WPMU_PLUGIN_DIR ) . '</code>'
  576. ) . '</p>';
  577. } elseif ( 'top' === $which && 'dropins' === $status ) {
  578. echo '<p>' . sprintf(
  579. /* translators: %s: wp-content directory name. */
  580. __( 'Drop-ins are single files, found in the %s directory, that replace or enhance WordPress features in ways that are not possible for traditional plugins.' ),
  581. '<code>' . str_replace( ABSPATH, '', WP_CONTENT_DIR ) . '</code>'
  582. ) . '</p>';
  583. }
  584. echo '</div>';
  585. }
  586. /**
  587. * @return string
  588. */
  589. public function current_action() {
  590. if ( isset( $_POST['clear-recent-list'] ) ) {
  591. return 'clear-recent-list';
  592. }
  593. return parent::current_action();
  594. }
  595. /**
  596. * @global string $status
  597. */
  598. public function display_rows() {
  599. global $status;
  600. if ( is_multisite() && ! $this->screen->in_admin( 'network' ) && in_array( $status, array( 'mustuse', 'dropins' ), true ) ) {
  601. return;
  602. }
  603. foreach ( $this->items as $plugin_file => $plugin_data ) {
  604. $this->single_row( array( $plugin_file, $plugin_data ) );
  605. }
  606. }
  607. /**
  608. * @global string $status
  609. * @global int $page
  610. * @global string $s
  611. * @global array $totals
  612. *
  613. * @param array $item
  614. */
  615. public function single_row( $item ) {
  616. global $status, $page, $s, $totals;
  617. static $plugin_id_attrs = array();
  618. list( $plugin_file, $plugin_data ) = $item;
  619. $plugin_slug = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_data['Name'] );
  620. $plugin_id_attr = $plugin_slug;
  621. // Ensure the ID attribute is unique.
  622. $suffix = 2;
  623. while ( in_array( $plugin_id_attr, $plugin_id_attrs, true ) ) {
  624. $plugin_id_attr = "$plugin_slug-$suffix";
  625. $suffix++;
  626. }
  627. $plugin_id_attrs[] = $plugin_id_attr;
  628. $context = $status;
  629. $screen = $this->screen;
  630. // Pre-order.
  631. $actions = array(
  632. 'deactivate' => '',
  633. 'activate' => '',
  634. 'details' => '',
  635. 'delete' => '',
  636. );
  637. // Do not restrict by default.
  638. $restrict_network_active = false;
  639. $restrict_network_only = false;
  640. if ( 'mustuse' === $context ) {
  641. $is_active = true;
  642. } elseif ( 'dropins' === $context ) {
  643. $dropins = _get_dropins();
  644. $plugin_name = $plugin_file;
  645. if ( $plugin_file !== $plugin_data['Name'] ) {
  646. $plugin_name .= '<br/>' . $plugin_data['Name'];
  647. }
  648. if ( true === ( $dropins[ $plugin_file ][1] ) ) { // Doesn't require a constant.
  649. $is_active = true;
  650. $description = '<p><strong>' . $dropins[ $plugin_file ][0] . '</strong></p>';
  651. } elseif ( defined( $dropins[ $plugin_file ][1] ) && constant( $dropins[ $plugin_file ][1] ) ) { // Constant is true.
  652. $is_active = true;
  653. $description = '<p><strong>' . $dropins[ $plugin_file ][0] . '</strong></p>';
  654. } else {
  655. $is_active = false;
  656. $description = '<p><strong>' . $dropins[ $plugin_file ][0] . ' <span class="error-message">' . __( 'Inactive:' ) . '</span></strong> ' .
  657. sprintf(
  658. /* translators: 1: Drop-in constant name, 2: wp-config.php */
  659. __( 'Requires %1$s in %2$s file.' ),
  660. "<code>define('" . $dropins[ $plugin_file ][1] . "', true);</code>",
  661. '<code>wp-config.php</code>'
  662. ) . '</p>';
  663. }
  664. if ( $plugin_data['Description'] ) {
  665. $description .= '<p>' . $plugin_data['Description'] . '</p>';
  666. }
  667. } else {
  668. if ( $screen->in_admin( 'network' ) ) {
  669. $is_active = is_plugin_active_for_network( $plugin_file );
  670. } else {
  671. $is_active = is_plugin_active( $plugin_file );
  672. $restrict_network_active = ( is_multisite() && is_plugin_active_for_network( $plugin_file ) );
  673. $restrict_network_only = ( is_multisite() && is_network_only_plugin( $plugin_file ) && ! $is_active );
  674. }
  675. if ( $screen->in_admin( 'network' ) ) {
  676. if ( $is_active ) {
  677. if ( current_user_can( 'manage_network_plugins' ) ) {
  678. $actions['deactivate'] = sprintf(
  679. '<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
  680. wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'deactivate-plugin_' . $plugin_file ),
  681. esc_attr( $plugin_id_attr ),
  682. /* translators: %s: Plugin name. */
  683. esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
  684. __( 'Network Deactivate' )
  685. );
  686. }
  687. } else {
  688. if ( current_user_can( 'manage_network_plugins' ) ) {
  689. $actions['activate'] = sprintf(
  690. '<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
  691. wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'activate-plugin_' . $plugin_file ),
  692. esc_attr( $plugin_id_attr ),
  693. /* translators: %s: Plugin name. */
  694. esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
  695. __( 'Network Activate' )
  696. );
  697. }
  698. if ( current_user_can( 'delete_plugins' ) && ! is_plugin_active( $plugin_file ) ) {
  699. $actions['delete'] = sprintf(
  700. '<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
  701. wp_nonce_url( 'plugins.php?action=delete-selected&amp;checked[]=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'bulk-plugins' ),
  702. esc_attr( $plugin_id_attr ),
  703. /* translators: %s: Plugin name. */
  704. esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
  705. __( 'Delete' )
  706. );
  707. }
  708. }
  709. } else {
  710. if ( $restrict_network_active ) {
  711. $actions = array(
  712. 'network_active' => __( 'Network Active' ),
  713. );
  714. } elseif ( $restrict_network_only ) {
  715. $actions = array(
  716. 'network_only' => __( 'Network Only' ),
  717. );
  718. } elseif ( $is_active ) {
  719. if ( current_user_can( 'deactivate_plugin', $plugin_file ) ) {
  720. $actions['deactivate'] = sprintf(
  721. '<a href="%s" id="deactivate-%s" aria-label="%s">%s</a>',
  722. wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'deactivate-plugin_' . $plugin_file ),
  723. esc_attr( $plugin_id_attr ),
  724. /* translators: %s: Plugin name. */
  725. esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ),
  726. __( 'Deactivate' )
  727. );
  728. }
  729. if ( current_user_can( 'resume_plugin', $plugin_file ) && is_plugin_paused( $plugin_file ) ) {
  730. $actions['resume'] = sprintf(
  731. '<a href="%s" id="resume-%s" class="resume-link" aria-label="%s">%s</a>',
  732. wp_nonce_url( 'plugins.php?action=resume&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'resume-plugin_' . $plugin_file ),
  733. esc_attr( $plugin_id_attr ),
  734. /* translators: %s: Plugin name. */
  735. esc_attr( sprintf( _x( 'Resume %s', 'plugin' ), $plugin_data['Name'] ) ),
  736. __( 'Resume' )
  737. );
  738. }
  739. } else {
  740. if ( current_user_can( 'activate_plugin', $plugin_file ) ) {
  741. $actions['activate'] = sprintf(
  742. '<a href="%s" id="activate-%s" class="edit" aria-label="%s">%s</a>',
  743. wp_nonce_url( 'plugins.php?action=activate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'activate-plugin_' . $plugin_file ),
  744. esc_attr( $plugin_id_attr ),
  745. /* translators: %s: Plugin name. */
  746. esc_attr( sprintf( _x( 'Activate %s', 'plugin' ), $plugin_data['Name'] ) ),
  747. __( 'Activate' )
  748. );
  749. }
  750. if ( ! is_multisite() && current_user_can( 'delete_plugins' ) ) {
  751. $actions['delete'] = sprintf(
  752. '<a href="%s" id="delete-%s" class="delete" aria-label="%s">%s</a>',
  753. wp_nonce_url( 'plugins.php?action=delete-selected&amp;checked[]=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'bulk-plugins' ),
  754. esc_attr( $plugin_id_attr ),
  755. /* translators: %s: Plugin name. */
  756. esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ),
  757. __( 'Delete' )
  758. );
  759. }
  760. } // End if $is_active.
  761. } // End if $screen->in_admin( 'network' ).
  762. } // End if $context.
  763. $actions = array_filter( $actions );
  764. if ( $screen->in_admin( 'network' ) ) {
  765. /**
  766. * Filters the action links displayed for each plugin in the Network Admin Plugins list table.
  767. *
  768. * @since 3.1.0
  769. *
  770. * @param string[] $actions An array of plugin action links. By default this can include 'activate',
  771. * 'deactivate', and 'delete'.
  772. * @param string $plugin_file Path to the plugin file relative to the plugins directory.
  773. * @param array $plugin_data An array of plugin data. See `get_plugin_data()`.
  774. * @param string $context The plugin context. By default this can include 'all', 'active', 'inactive',
  775. * 'recently_activated', 'upgrade', 'mustuse', 'dropins', and 'search'.
  776. */
  777. $actions = apply_filters( 'network_admin_plugin_action_links', $actions, $plugin_file, $plugin_data, $context );
  778. /**
  779. * Filters the list of action links displayed for a specific plugin in the Network Admin Plugins list table.
  780. *
  781. * The dynamic portion of the hook name, `$plugin_file`, refers to the path
  782. * to the plugin file, relative to the plugins directory.
  783. *
  784. * @since 3.1.0
  785. *
  786. * @param string[] $actions An array of plugin action links. By default this can include 'activate',
  787. * 'deactivate', and 'delete'.
  788. * @param string $plugin_file Path to the plugin file relative to the plugins directory.
  789. * @param array $plugin_data An array of plugin data. See `get_plugin_data()`.
  790. * @param string $context The plugin context. By default this can include 'all', 'active', 'inactive',
  791. * 'recently_activated', 'upgrade', 'mustuse', 'dropins', and 'search'.
  792. */
  793. $actions = apply_filters( "network_admin_plugin_action_links_{$plugin_file}", $actions, $plugin_file, $plugin_data, $context );
  794. } else {
  795. /**
  796. * Filters the action links displayed for each plugin in the Plugins list table.
  797. *
  798. * @since 2.5.0
  799. * @since 2.6.0 The `$context` parameter was added.
  800. * @since 4.9.0 The 'Edit' link was removed from the list of action links.
  801. *
  802. * @param string[] $actions An array of plugin action links. By default this can include 'activate',
  803. * 'deactivate', and 'delete'. With Multisite active this can also include
  804. * 'network_active' and 'network_only' items.
  805. * @param string $plugin_file Path to the plugin file relative to the plugins directory.
  806. * @param array $plugin_data An array of plugin data. See `get_plugin_data()`.
  807. * @param string $context The plugin context. By default this can include 'all', 'active', 'inactive',
  808. * 'recently_activated', 'upgrade', 'mustuse', 'dropins', and 'search'.
  809. */
  810. $actions = apply_filters( 'plugin_action_links', $actions, $plugin_file, $plugin_data, $context );
  811. /**
  812. * Filters the list of action links displayed for a specific plugin in the Plugins list table.
  813. *
  814. * The dynamic portion of the hook name, `$plugin_file`, refers to the path
  815. * to the plugin file, relative to the plugins directory.
  816. *
  817. * @since 2.7.0
  818. * @since 4.9.0 The 'Edit' link was removed from the list of action links.
  819. *
  820. * @param string[] $actions An array of plugin action links. By default this can include 'activate',
  821. * 'deactivate', and 'delete'. With Multisite active this can also include
  822. * 'network_active' and 'network_only' items.
  823. * @param string $plugin_file Path to the plugin file relative to the plugins directory.
  824. * @param array $plugin_data An array of plugin data. See `get_plugin_data()`.
  825. * @param string $context The plugin context. By default this can include 'all', 'active', 'inactive',
  826. * 'recently_activated', 'upgrade', 'mustuse', 'dropins', and 'search'.
  827. */
  828. $actions = apply_filters( "plugin_action_links_{$plugin_file}", $actions, $plugin_file, $plugin_data, $context );
  829. }
  830. $requires_php = isset( $plugin_data['requires_php'] ) ? $plugin_data['requires_php'] : null;
  831. $compatible_php = is_php_version_compatible( $requires_php );
  832. $class = $is_active ? 'active' : 'inactive';
  833. $checkbox_id = 'checkbox_' . md5( $plugin_file );
  834. if ( $restrict_network_active || $restrict_network_only || in_array( $status, array( 'mustuse', 'dropins' ), true ) || ! $compatible_php ) {
  835. $checkbox = '';
  836. } else {
  837. $checkbox = sprintf(
  838. '<label class="screen-reader-text" for="%1$s">%2$s</label>' .
  839. '<input type="checkbox" name="checked[]" value="%3$s" id="%1$s" />',
  840. $checkbox_id,
  841. /* translators: %s: Plugin name. */
  842. sprintf( __( 'Select %s' ), $plugin_data['Name'] ),
  843. esc_attr( $plugin_file )
  844. );
  845. }
  846. if ( 'dropins' !== $context ) {
  847. $description = '<p>' . ( $plugin_data['Description'] ? $plugin_data['Description'] : '&nbsp;' ) . '</p>';
  848. $plugin_name = $plugin_data['Name'];
  849. }
  850. if ( ! empty( $totals['upgrade'] ) && ! empty( $plugin_data['update'] ) ) {
  851. $class .= ' update';
  852. }
  853. $paused = ! $screen->in_admin( 'network' ) && is_plugin_paused( $plugin_file );
  854. if ( $paused ) {
  855. $class .= ' paused';
  856. }
  857. if ( is_uninstallable_plugin( $plugin_file ) ) {
  858. $class .= ' is-uninstallable';
  859. }
  860. printf(
  861. '<tr class="%s" data-slug="%s" data-plugin="%s">',
  862. esc_attr( $class ),
  863. esc_attr( $plugin_slug ),
  864. esc_attr( $plugin_file )
  865. );
  866. list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
  867. $auto_updates = (array) get_site_option( 'auto_update_plugins', array() );
  868. $available_updates = get_site_transient( 'update_plugins' );
  869. foreach ( $columns as $column_name => $column_display_name ) {
  870. $extra_classes = '';
  871. if ( in_array( $column_name, $hidden, true ) ) {
  872. $extra_classes = ' hidden';
  873. }
  874. switch ( $column_name ) {
  875. case 'cb':
  876. echo "<th scope='row' class='check-column'>$checkbox</th>";
  877. break;
  878. case 'name':
  879. echo "<td class='plugin-title column-primary'><strong>$plugin_name</strong>";
  880. echo $this->row_actions( $actions, true );
  881. echo '</td>';
  882. break;
  883. case 'description':
  884. $classes = 'column-description desc';
  885. echo "<td class='$classes{$extra_classes}'>
  886. <div class='plugin-description'>$description</div>
  887. <div class='$class second plugin-version-author-uri'>";
  888. $plugin_meta = array();
  889. if ( ! empty( $plugin_data['Version'] ) ) {
  890. /* translators: %s: Plugin version number. */
  891. $plugin_meta[] = sprintf( __( 'Version %s' ), $plugin_data['Version'] );
  892. }
  893. if ( ! empty( $plugin_data['Author'] ) ) {
  894. $author = $plugin_data['Author'];
  895. if ( ! empty( $plugin_data['AuthorURI'] ) ) {
  896. $author = '<a href="' . $plugin_data['AuthorURI'] . '">' . $plugin_data['Author'] . '</a>';
  897. }
  898. /* translators: %s: Plugin author name. */
  899. $plugin_meta[] = sprintf( __( 'By %s' ), $author );
  900. }
  901. // Details link using API info, if available.
  902. if ( isset( $plugin_data['slug'] ) && current_user_can( 'install_plugins' ) ) {
  903. $plugin_meta[] = sprintf(
  904. '<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
  905. esc_url(
  906. network_admin_url(
  907. 'plugin-install.php?tab=plugin-information&plugin=' . $plugin_data['slug'] .
  908. '&TB_iframe=true&width=600&height=550'
  909. )
  910. ),
  911. /* translators: %s: Plugin name. */
  912. esc_attr( sprintf( __( 'More information about %s' ), $plugin_name ) ),
  913. esc_attr( $plugin_name ),
  914. __( 'View details' )
  915. );
  916. } elseif ( ! empty( $plugin_data['PluginURI'] ) ) {
  917. $plugin_meta[] = sprintf(
  918. '<a href="%s">%s</a>',
  919. esc_url( $plugin_data['PluginURI'] ),
  920. __( 'Visit plugin site' )
  921. );
  922. }
  923. /**
  924. * Filters the array of row meta for each plugin in the Plugins list table.
  925. *
  926. * @since 2.8.0
  927. *
  928. * @param string[] $plugin_meta An array of the plugin's metadata, including
  929. * the version, author, author URI, and plugin URI.
  930. * @param string $plugin_file Path to the plugin file relative to the plugins directory.
  931. * @param array $plugin_data An array of plugin data.
  932. * @param string $status Status filter currently applied to the plugin list. Possible
  933. * values are: 'all', 'active', 'inactive', 'recently_activated',
  934. * 'upgrade', 'mustuse', 'dropins', 'search', 'paused',
  935. * 'auto-update-enabled', 'auto-update-disabled'.
  936. */
  937. $plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status );
  938. echo implode( ' | ', $plugin_meta );
  939. echo '</div>';
  940. if ( $paused ) {
  941. $notice_text = __( 'This plugin failed to load properly and is paused during recovery mode.' );
  942. printf( '<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>', $notice_text );
  943. $error = wp_get_plugin_error( $plugin_file );
  944. if ( false !== $error ) {
  945. printf( '<div class="error-display"><p>%s</p></div>', wp_get_extension_error_description( $error ) );
  946. }
  947. }
  948. echo '</td>';
  949. break;
  950. case 'auto-updates':
  951. if ( ! $this->show_autoupdates ) {
  952. break;
  953. }
  954. echo "<td class='column-auto-updates{$extra_classes}'>";
  955. $html = array();
  956. if ( isset( $plugin_data['auto-update-forced'] ) ) {
  957. if ( $plugin_data['auto-update-forced'] ) {
  958. // Forced on.
  959. $text = __( 'Auto-updates enabled' );
  960. } else {
  961. $text = __( 'Auto-updates disabled' );
  962. }
  963. $action = 'unavailable';
  964. $time_class = ' hidden';
  965. } elseif ( empty( $plugin_data['update-supported'] ) ) {
  966. $text = '';
  967. $action = 'unavailable';
  968. $time_class = ' hidden';
  969. } elseif ( in_array( $plugin_file, $auto_updates, true ) ) {
  970. $text = __( 'Disable auto-updates' );
  971. $action = 'disable';
  972. $time_class = '';
  973. } else {
  974. $text = __( 'Enable auto-updates' );
  975. $action = 'enable';
  976. $time_class = ' hidden';
  977. }
  978. $query_args = array(
  979. 'action' => "{$action}-auto-update",
  980. 'plugin' => $plugin_file,
  981. 'paged' => $page,
  982. 'plugin_status' => $status,
  983. );
  984. $url = add_query_arg( $query_args, 'plugins.php' );
  985. if ( 'unavailable' === $action ) {
  986. $html[] = '<span class="label">' . $text . '</span>';
  987. } else {
  988. $html[] = sprintf(
  989. '<a href="%s" class="toggle-auto-update aria-button-if-js" data-wp-action="%s">',
  990. wp_nonce_url( $url, 'updates' ),
  991. $action
  992. );
  993. $html[] = '<span class="dashicons dashicons-update spin hidden" aria-hidden="true"></span>';
  994. $html[] = '<span class="label">' . $text . '</span>';
  995. $html[] = '</a>';
  996. }
  997. if ( ! empty( $plugin_data['update'] ) ) {
  998. $html[] = sprintf(
  999. '<div class="auto-update-time%s">%s</div>',
  1000. $time_class,
  1001. wp_get_auto_update_message()
  1002. );
  1003. }
  1004. $html = implode( '', $html );
  1005. /**
  1006. * Filters the HTML of the auto-updates setting for each plugin in the Plugins list table.
  1007. *
  1008. * @since 5.5.0
  1009. *
  1010. * @param string $html The HTML of the plugin's auto-update column content, including
  1011. * toggle auto-update action links and time to next update.
  1012. * @param string $plugin_file Path to the plugin file relative to the plugins directory.
  1013. * @param array $plugin_data An array of plugin data.
  1014. */
  1015. echo apply_filters( 'plugin_auto_update_setting_html', $html, $plugin_file, $plugin_data );
  1016. echo '<div class="notice notice-error notice-alt inline hidden"><p></p></div>';
  1017. echo '</td>';
  1018. break;
  1019. default:
  1020. $classes = "$column_name column-$column_name $class";
  1021. echo "<td class='$classes{$extra_classes}'>";
  1022. /**
  1023. * Fires inside each custom column of the Plugins list table.
  1024. *
  1025. * @since 3.1.0
  1026. *
  1027. * @param string $column_name Name of the column.
  1028. * @param string $plugin_file Path to the plugin file relative to the plugins directory.
  1029. * @param array $plugin_data An array of plugin data.
  1030. */
  1031. do_action( 'manage_plugins_custom_column', $column_name, $plugin_file, $plugin_data );
  1032. echo '</td>';
  1033. }
  1034. }
  1035. echo '</tr>';
  1036. /**
  1037. * Fires after each row in the Plugins list table.
  1038. *
  1039. * @since 2.3.0
  1040. * @since 5.5.0 Added 'auto-update-enabled' and 'auto-update-disabled' to possible values for `$status`.
  1041. *
  1042. * @param string $plugin_file Path to the plugin file relative to the plugins directory.
  1043. * @param array $plugin_data An array of plugin data.
  1044. * @param string $status Status filter currently applied to the plugin list. Possible
  1045. * values are: 'all', 'active', 'inactive', 'recently_activated',
  1046. * 'upgrade', 'mustuse', 'dropins', 'search', 'paused',
  1047. * 'auto-update-enabled', 'auto-update-disabled'.
  1048. */
  1049. do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status );
  1050. /**
  1051. * Fires after each specific row in the Plugins list table.
  1052. *
  1053. * The dynamic portion of the hook name, `$plugin_file`, refers to the path
  1054. * to the plugin file, relative to the plugins directory.
  1055. *
  1056. * @since 2.7.0
  1057. * @since 5.5.0 Added 'auto-update-enabled' and 'auto-update-disabled' to possible values for `$status`.
  1058. *
  1059. * @param string $plugin_file Path to the plugin file relative to the plugins directory.
  1060. * @param array $plugin_data An array of plugin data.
  1061. * @param string $status Status filter currently applied to the plugin list. Possible
  1062. * values are: 'all', 'active', 'inactive', 'recently_activated',
  1063. * 'upgrade', 'mustuse', 'dropins', 'search', 'paused',
  1064. * 'auto-update-enabled', 'auto-update-disabled'.
  1065. */
  1066. do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status );
  1067. }
  1068. /**
  1069. * Gets the name of the primary column for this specific list table.
  1070. *
  1071. * @since 4.3.0
  1072. *
  1073. * @return string Unalterable name for the primary column, in this case, 'name'.
  1074. */
  1075. protected function get_primary_column_name() {
  1076. return 'name';
  1077. }
  1078. }