Spaces:
Sleeping
Sleeping
| { | |
| "info": { | |
| "name": "AgriTech AI API", | |
| "description": "Complete API collection for the Plant Disease Prediction system.\n\n## Setup\n1. Import this collection into Postman\n2. Set `baseUrl` to `http://localhost:8000` (or your LAN IP)\n3. Run \"1. Login\" \u2014 JWT auto-saves to `token`\n4. Run \"2. Login as Admin\" \u2014 admin JWT saves to `adminToken`\n\n## Admin endpoints\nUse the admin token (requires an is_staff user). Create one:\n`python manage.py createsuperuser`\n\n## Test Flow Order\n1. Login (get user token)\n2. Predict a leaf photo \u2192 check confidence_percent, disease_info\n3. Login as Admin (get admin token)\n4. List all predictions \u2192 find prediction ID\n5. Reclassify Unknown \u2192 assign real class\n6. Verify prediction \u2192 confirm disease created\n7. Search/Get disease detail \u2192 see description, treatment, partners\n8. Trigger retraining \u2192 model learns new class\n\n## New Feature: Suggested Species\nWhen a leaf is \"Unknown\", Model 2 (HuggingFace) identifies the plant species. Check the `suggested_species` field in the response.", | |
| "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" | |
| }, | |
| "variable": [ | |
| { | |
| "key": "baseUrl", | |
| "value": "http://localhost:8000" | |
| }, | |
| { | |
| "key": "token", | |
| "value": "" | |
| }, | |
| { | |
| "key": "adminToken", | |
| "value": "" | |
| }, | |
| { | |
| "key": "predictionId", | |
| "value": "" | |
| }, | |
| { | |
| "key": "adminPredictionId", | |
| "value": "" | |
| }, | |
| { | |
| "key": "refreshToken", | |
| "value": "" | |
| }, | |
| { | |
| "key": "diseaseId", | |
| "value": "" | |
| }, | |
| { | |
| "key": "newDiseaseClass", | |
| "value": "" | |
| } | |
| ], | |
| "item": [ | |
| { | |
| "name": "Auth", | |
| "item": [ | |
| { | |
| "name": "1. Login \u2014 Get JWT Token", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " pm.collectionVariables.set('token', json.access);", | |
| " pm.collectionVariables.set('refreshToken', json.refresh);", | |
| " console.log('Access token set:', json.access.substring(0, 20) + '...');", | |
| "} else {", | |
| " console.log('Login failed:', pm.response.code, JSON.stringify(pm.response.json()));", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Content-Type", | |
| "value": "application/json" | |
| } | |
| ], | |
| "body": { | |
| "mode": "raw", | |
| "raw": "{\n \"username\": \"testuser\",\n \"password\": \"testpass123\"\n}" | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/token/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "token", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "2. Login as Admin \u2014 Get JWT Token", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " pm.collectionVariables.set('adminToken', json.access);", | |
| " pm.collectionVariables.set('refreshToken', json.refresh);", | |
| " console.log('Admin token set:', json.access.substring(0, 20) + '...');", | |
| " pm.test('Admin login successful', function() {", | |
| " pm.expect(json.access).to.not.be.empty;", | |
| " });", | |
| "} else {", | |
| " console.log('Admin login failed. Create a superuser with: python manage.py createsuperuser');", | |
| " console.log('Response:', pm.response.code, JSON.stringify(pm.response.json()));", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Content-Type", | |
| "value": "application/json" | |
| } | |
| ], | |
| "body": { | |
| "mode": "raw", | |
| "raw": "{\n \"username\": \"admin\",\n \"password\": \"admin123\"\n}" | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/token/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "token", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "3. Signup \u2014 Create Account", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 201) {", | |
| " var json = pm.response.json();", | |
| " console.log('User created:', json.user.username);", | |
| " pm.test('Signup creates user with ID', function() {", | |
| " pm.expect(json.user.id).to.be.a('number');", | |
| " });", | |
| "} else if (pm.response.code === 400) {", | |
| " console.log('Signup validation error:', JSON.stringify(pm.response.json()));", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Content-Type", | |
| "value": "application/json" | |
| } | |
| ], | |
| "body": { | |
| "mode": "raw", | |
| "raw": "{\n \"username\": \"newuser\",\n \"email\": \"newuser@example.com\",\n \"password\": \"securepass123\"\n}" | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/signup/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "signup", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "4. Refresh Token", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " pm.collectionVariables.set('token', json.access);", | |
| " console.log('New access token set:', json.access.substring(0, 20) + '...');", | |
| " pm.test('Refresh returns new access token', function() {", | |
| " pm.expect(json.access).to.not.be.empty;", | |
| " });", | |
| "} else {", | |
| " console.log('Refresh failed:', pm.response.code, JSON.stringify(pm.response.json()));", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Content-Type", | |
| "value": "application/json" | |
| } | |
| ], | |
| "body": { | |
| "mode": "raw", | |
| "raw": "{\n \"refresh\": \"{{refreshToken}}\"\n}" | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/token/refresh/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "token", | |
| "refresh", | |
| "" | |
| ] | |
| } | |
| } | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "Reference Data", | |
| "item": [ | |
| { | |
| "name": "5. List Diseases", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " console.log('Diseases found:', json.count || json.results.length);", | |
| " pm.test('Response has results array', function() {", | |
| " pm.expect(json.results).to.be.an('array');", | |
| " });", | |
| " if (json.results.length > 0) {", | |
| " pm.collectionVariables.set('diseaseId', json.results[0].id);", | |
| " console.log('First disease ID:', json.results[0].id);", | |
| " pm.test('Disease has description', function() {", | |
| " pm.expect(json.results[0].description).to.not.be.empty;", | |
| " });", | |
| " }", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "GET", | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/diseases/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "diseases", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "6. Search Diseases", | |
| "description": "Search diseases by name. Uses DRF SearchFilter.", | |
| "request": { | |
| "method": "GET", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{token}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/diseases/?search=apple", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "diseases", | |
| "" | |
| ], | |
| "query": [ | |
| { | |
| "key": "search", | |
| "value": "apple" | |
| } | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "6b. Get Disease Detail", | |
| "description": "Get full disease details including description, treatment (chemical + organic cure, prevention tips) and partner companies. Tests that symptoms and treatment info is available.", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " console.log('Disease:', json.common_name);", | |
| " console.log('Description:', json.description.substring(0, 80) + '...');", | |
| " pm.test('Disease has description (symptoms info)', function() {", | |
| " pm.expect(json.description).to.not.be.empty;", | |
| " });", | |
| " if (json.treatment) {", | |
| " console.log('Chemical cure:', json.treatment.chemical_cure);", | |
| " console.log('Organic cure:', json.treatment.organic_cure);", | |
| " console.log('Prevention tips:', json.treatment.prevention_tips);", | |
| " pm.test('Treatment has chemical cure (medicines)', function() {", | |
| " pm.expect(json.treatment.chemical_cure).to.not.be.empty;", | |
| " });", | |
| " pm.test('Treatment has organic cure', function() {", | |
| " pm.expect(json.treatment.organic_cure).to.not.be.empty;", | |
| " });", | |
| " pm.test('Treatment has prevention tips', function() {", | |
| " pm.expect(json.treatment.prevention_tips).to.not.be.empty;", | |
| " });", | |
| " }", | |
| " if (json.partners && json.partners.length > 0) {", | |
| " console.log('Partner companies:', json.partners.length);", | |
| " pm.test('Partners include company names', function() {", | |
| " pm.expect(json.partners[0].name).to.not.be.empty;", | |
| " });", | |
| " }", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "GET", | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/diseases/{{diseaseId}}/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "diseases", | |
| "{{diseaseId}}", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "7. List Treatments", | |
| "request": { | |
| "method": "GET", | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/treatments/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "treatments", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "8. List Companies", | |
| "request": { | |
| "method": "GET", | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/companies/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "companies", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "11b. Search Disease by Species", | |
| "description": "Search diseases by plant species name. Tests that species-linked disease records are searchable in the API.", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " console.log('Results for species search:', json.count || json.results.length);", | |
| " pm.test('Search returns results', function() {", | |
| " pm.expect(json.results).to.be.an('array');", | |
| " });", | |
| " if (json.results.length > 0) {", | |
| " json.results.forEach(function(d) {", | |
| " console.log(' -', d.class_name, '(' + d.plant_type + ')');", | |
| " pm.test('Disease ' + d.class_name + ' has plant_type', function() {", | |
| " pm.expect(d.plant_type).to.not.be.empty;", | |
| " });", | |
| " });", | |
| " } else {", | |
| " console.log('No results for this species. Try a different search term.');", | |
| " }", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "GET", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{token}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/diseases/?search=Tomato", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "diseases", | |
| "" | |
| ], | |
| "query": [ | |
| { | |
| "key": "search", | |
| "value": "Tomato" | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "Prediction", | |
| "item": [ | |
| { | |
| "name": "9. Predict Disease (Upload Photo)", | |
| "description": "Upload a leaf photo.\n\n- 201 \u2192 success with `data`, `disease_info`\n- 400 \u2192 error (NonPlant, oversized, wrong type)\n- 500 \u2192 inference failure\n\n## Test Results (asserted in test script)\n- `confidence_percent` is always returned as a number\n- `suggested_species` is present when prediction is Unknown\n- `disease_info` includes treatment (chemical_cure, organic_cure, prevention_tips) when prediction matches a known disease\n\n## Percentage Test\nConfidence is stored as a float (0-1) in the DB and returned as `confidence_percent` (0-100) in the response.", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 201) {", | |
| " var json = pm.response.json();", | |
| " var data = json.data;", | |
| " pm.collectionVariables.set('predictionId', data.id);", | |
| " console.log('Prediction ID:', data.id);", | |
| " console.log('Class:', data.predicted_class);", | |
| " console.log('Confidence:', data.confidence);", | |
| " console.log('Confidence %:', data.confidence_percent + '%');", | |
| "", | |
| " // TEST: confidence_percent is always returned", | |
| " pm.test('confidence_percent is a number', function() {", | |
| " pm.expect(data.confidence_percent).to.be.a('number');", | |
| " });", | |
| " pm.test('confidence_percent = confidence * 100', function() {", | |
| " pm.expect(data.confidence_percent).to.equal(data.confidence * 100);", | |
| " });", | |
| "", | |
| " // TEST: suggested_species is present (may be null for known, string for Unknown)", | |
| " pm.test('suggested_species field exists', function() {", | |
| " pm.expect(data).to.have.property('suggested_species');", | |
| " });", | |
| " if (data.predicted_class === 'Unknown') {", | |
| " console.log('Suggested species:', data.suggested_species);", | |
| " pm.test('suggested_species is populated for Unknown', function() {", | |
| " pm.expect(data.suggested_species).to.not.be.null;", | |
| " });", | |
| " }", | |
| "", | |
| " // TEST: disease_info includes treatment (symptoms, medicines)", | |
| " if (json.disease_info) {", | |
| " console.log('Disease info found for:', data.predicted_class);", | |
| " if (json.disease_info.treatment) {", | |
| " console.log('Chemical cure:', json.disease_info.treatment.chemical_cure);", | |
| " console.log('Organic cure:', json.disease_info.treatment.organic_cure);", | |
| " console.log('Prevention tips:', json.disease_info.treatment.prevention_tips);", | |
| " pm.test('disease_info has chemical cure', function() {", | |
| " pm.expect(json.disease_info.treatment.chemical_cure).to.not.be.empty;", | |
| " });", | |
| " pm.test('disease_info has organic cure', function() {", | |
| " pm.expect(json.disease_info.treatment.organic_cure).to.not.be.empty;", | |
| " });", | |
| " pm.test('disease_info has prevention tips', function() {", | |
| " pm.expect(json.disease_info.treatment.prevention_tips).to.not.be.empty;", | |
| " });", | |
| " } else {", | |
| " console.log('No treatment record for this disease yet.');", | |
| " }", | |
| " } else {", | |
| " console.log('No disease_info (Unknown or no matching disease record)');", | |
| " }", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{token}}" | |
| } | |
| ], | |
| "body": { | |
| "mode": "formdata", | |
| "formdata": [ | |
| { | |
| "key": "image", | |
| "type": "file", | |
| "src": "" | |
| } | |
| ] | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/predictions/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "predictions", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "9b. Predict Unknown & Verify Species", | |
| "description": "Upload a leaf photo that will be classified as Unknown. Tests that the HuggingFace Model 2 identifies the plant species and populates `suggested_species`. This validates the two-model pipeline: Model 1 (TF) returns Unknown, Model 2 (HF) returns species name.", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 201) {", | |
| " var json = pm.response.json();", | |
| " var data = json.data;", | |
| " pm.collectionVariables.set('predictionId', data.id);", | |
| " console.log('Prediction ID:', data.id);", | |
| " console.log('Predicted class:', data.predicted_class);", | |
| " console.log('Suggested species:', data.suggested_species);", | |
| " console.log('Confidence:', data.confidence);", | |
| "", | |
| " if (data.predicted_class === 'Unknown') {", | |
| " pm.test('Unknown prediction has suggested_species', function() {", | |
| " pm.expect(data.suggested_species).to.not.be.null;", | |
| " pm.expect(data.suggested_species).to.not.be.empty;", | |
| " });", | |
| " console.log('Model 2 identified species:', data.suggested_species);", | |
| " pm.collectionVariables.set('newDiseaseClass', data.suggested_species + '__Healthy');", | |
| " console.log('Admin can accept as:', data.suggested_species + '__Healthy');", | |
| " } else {", | |
| " console.log('Prediction had a match (not Unknown). Try a different leaf photo.');", | |
| " }", | |
| "", | |
| " pm.test('Response includes predicted_class', function() {", | |
| " pm.expect(data.predicted_class).to.not.be.empty;", | |
| " });", | |
| " pm.test('Response includes confidence', function() {", | |
| " pm.expect(data.confidence).to.be.a('number');", | |
| " });", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{token}}" | |
| } | |
| ], | |
| "body": { | |
| "mode": "formdata", | |
| "formdata": [ | |
| { | |
| "key": "image", | |
| "type": "file", | |
| "src": "" | |
| } | |
| ] | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/predictions/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "predictions", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "9c. Predict Non-Plant Photo (Test Model Validation)", | |
| "description": "Upload an image that is not a plant (e.g. car, face, empty wall). The API's built-in `_is_likely_plant` AI function should detect the lack of green edge density and reject it with a 400 Bad Request.\n\nThis tests the first layer of the AI validation pipeline.", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "pm.test('Response should be 400 Bad Request', function() {", | |
| " pm.response.to.have.status(400);", | |
| "});", | |
| "pm.test('Response should say NonPlant', function() {", | |
| " var json = pm.response.json();", | |
| " pm.expect(json.predicted_class).to.equal('NonPlant');", | |
| "});" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{token}}" | |
| } | |
| ], | |
| "body": { | |
| "mode": "formdata", | |
| "formdata": [ | |
| { | |
| "key": "image", | |
| "type": "file", | |
| "src": "" | |
| } | |
| ] | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/predictions/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "predictions", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "9d. Predict Out-of-Distribution Leaf (Strict Novelty)", | |
| "description": "Upload a leaf that is NOT in the 38 classes (e.g. Guava leaf). This tests the new strict 0.65 novelty threshold and 95% confidence threshold.\n\nEven if the TF model tries to confidently guess 'Tomato Early Blight', the AI pipeline should override it and return 'Unknown'.", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "pm.test('Response should be 201 Created', function() {", | |
| " pm.response.to.have.status(201);", | |
| "});", | |
| "pm.test('Prediction should be Unknown', function() {", | |
| " var json = pm.response.json();", | |
| " pm.expect(json.data.predicted_class).to.equal('Unknown');", | |
| "});", | |
| "pm.test('Confidence should be logged correctly', function() {", | |
| " var json = pm.response.json();", | |
| " pm.expect(json.data.confidence_percent).to.be.a('number');", | |
| "});" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{token}}" | |
| } | |
| ], | |
| "body": { | |
| "mode": "formdata", | |
| "formdata": [ | |
| { | |
| "key": "image", | |
| "type": "file", | |
| "src": "" | |
| } | |
| ] | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/predictions/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "predictions", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "10. List My Predictions", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " console.log('My predictions:', json.count || json.results.length);", | |
| " pm.test('My predictions returns array', function() {", | |
| " pm.expect(json.results).to.be.an('array');", | |
| " });", | |
| " if (json.results.length > 0) {", | |
| " var p = json.results[0];", | |
| " console.log('Latest:', p.predicted_class, '(' + p.confidence_percent + '%)');", | |
| " pm.test('Prediction has confidence_percent', function() {", | |
| " pm.expect(p.confidence_percent).to.be.a('number');", | |
| " });", | |
| " pm.test('Prediction has suggested_species field', function() {", | |
| " pm.expect(p).to.have.property('suggested_species');", | |
| " });", | |
| " }", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "GET", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{token}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/predictions/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "predictions", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "11. Report Wrong Prediction", | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{token}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/predictions/{{predictionId}}/report_wrong/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "predictions", | |
| "{{predictionId}}", | |
| "report_wrong", | |
| "" | |
| ] | |
| } | |
| } | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "Admin", | |
| "item": [ | |
| { | |
| "name": "12. Admin \u2014 Dashboard Stats", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " console.log('Total users:', json.total_users);", | |
| " console.log('Total predictions:', json.total_predictions);", | |
| " console.log('Verified:', json.total_verified);", | |
| " console.log('Unknown pending:', json.unknown_predictions);", | |
| " pm.test('Stats returns all counts', function() {", | |
| " pm.expect(json.total_users).to.be.a('number');", | |
| " pm.expect(json.total_predictions).to.be.a('number');", | |
| " pm.expect(json.unknown_predictions).to.be.a('number');", | |
| " });", | |
| " pm.test('Stats includes retraining status', function() {", | |
| " pm.expect(json.retraining).to.have.property('status');", | |
| " });", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "GET", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{adminToken}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/admin/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "admin", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "13. Admin \u2014 List All Predictions", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " console.log('Total predictions:', json.count || json.results.length);", | |
| " pm.test('Admin list returns results', function() {", | |
| " pm.expect(json.results).to.be.an('array');", | |
| " });", | |
| " if (json.results.length > 0) {", | |
| " pm.collectionVariables.set('adminPredictionId', json.results[0].id);", | |
| " console.log('First prediction ID:', json.results[0].id);", | |
| " pm.test('Prediction has suggested_species', function() {", | |
| " pm.expect(json.results[0]).to.have.property('suggested_species');", | |
| " });", | |
| " }", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "GET", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{adminToken}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/admin/predictions/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "admin", | |
| "predictions", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "14. Admin \u2014 List Unverified Predictions", | |
| "request": { | |
| "method": "GET", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{adminToken}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/admin/predictions/?is_verified=False", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "admin", | |
| "predictions", | |
| "" | |
| ], | |
| "query": [ | |
| { | |
| "key": "is_verified", | |
| "value": "False" | |
| } | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "15. Admin \u2014 Verify Prediction", | |
| "description": "Verifies a prediction. Photo moves from unverified_dataset/ \u2192 verified_dataset/. Creates Disease record if it doesn't exist. Notifies the user. Tests that verification succeeds and returns the predicted_class.", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " console.log('Verified:', json.predicted_class);", | |
| " pm.test('Verify returns status = verified', function() {", | |
| " pm.expect(json.status).to.equal('verified');", | |
| " });", | |
| " pm.test('Verify returns predicted_class', function() {", | |
| " pm.expect(json.predicted_class).to.not.be.empty;", | |
| " });", | |
| "} else {", | |
| " console.log('Verify failed:', pm.response.code, JSON.stringify(pm.response.json()));", | |
| " console.log('Ensure prediction ID exists and is not already verified.');", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{adminToken}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/admin/predictions/{{adminPredictionId}}/verify/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "admin", | |
| "predictions", | |
| "{{adminPredictionId}}", | |
| "verify", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "16. Admin \u2014 Reject Prediction", | |
| "description": "Rejects a prediction. Photo moves back from verified_dataset/ \u2192 unverified_dataset/.", | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{adminToken}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/admin/predictions/{{adminPredictionId}}/reject/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "admin", | |
| "predictions", | |
| "{{adminPredictionId}}", | |
| "reject", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "17. Admin \u2014 Reclassify Unknown", | |
| "description": "Reclassifies an Unknown prediction to a new disease class.\n1. Prediction must have predicted_class = \"Unknown\"\n2. new_class must NOT already exist in the Disease table\n3. Photo moves from unverified_dataset/unknown/ \u2192 verified_dataset/{new_class}/\n4. A new Disease record is auto-created\n5. A new Treatment record can be added later via the admin portal\n\nSaves the new class name to {{newDiseaseClass}} for follow-up tests.", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " console.log('Reclassified to:', json.new_class);", | |
| " pm.collectionVariables.set('newDiseaseClass', json.new_class);", | |
| " pm.test('Reclassify returns new_class', function() {", | |
| " pm.expect(json.new_class).to.not.be.empty;", | |
| " });", | |
| " pm.test('Reclassify returns status', function() {", | |
| " pm.expect(json.status).to.equal('reclassified');", | |
| " });", | |
| "} else {", | |
| " console.log('Reclassify failed:', pm.response.code, JSON.stringify(pm.response.json()));", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Content-Type", | |
| "value": "application/json" | |
| }, | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{adminToken}}" | |
| } | |
| ], | |
| "body": { | |
| "mode": "raw", | |
| "raw": "{\n \"new_class\": \"New_Disease_Name\",\n \"common_name\": \"New Disease Name\",\n \"plant_type\": \"Tomato\",\n \"description\": \"Optional description for this new disease.\"\n}" | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/admin/predictions/{{adminPredictionId}}/reclassify/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "admin", | |
| "predictions", | |
| "{{adminPredictionId}}", | |
| "reclassify", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "17b. Admin \u2014 Verify Reclassify Creates New Disease", | |
| "description": "After reclassifying an Unknown prediction, search for the new disease class to confirm it was created in the database. Tests the full reclassify flow: Unknown \u2192 new class \u2192 Disease record exists.", | |
| "event": [ | |
| { | |
| "listen": "test", | |
| "script": { | |
| "exec": [ | |
| "if (pm.response.code === 200) {", | |
| " var json = pm.response.json();", | |
| " console.log('Search results:', json.count || json.results.length);", | |
| " var found = false;", | |
| " var newClass = pm.collectionVariables.get('newDiseaseClass');", | |
| " if (newClass) {", | |
| " json.results.forEach(function(d) {", | |
| " if (d.class_name === newClass) {", | |
| " found = true;", | |
| " console.log('Found new disease:', d.common_name);", | |
| " console.log('Description:', d.description.substring(0, 80) + '...');", | |
| " pm.test('New disease has description', function() {", | |
| " pm.expect(d.description).to.not.be.empty;", | |
| " });", | |
| " }", | |
| " });", | |
| " pm.test('Reclassified disease found in database', function() {", | |
| " pm.expect(found).to.be.true;", | |
| " });", | |
| " if (!found) {", | |
| " console.log('Disease ' + newClass + ' not found in results. Available:');", | |
| " json.results.forEach(function(d) { console.log(' -', d.class_name); });", | |
| " }", | |
| " } else {", | |
| " console.log('No newDiseaseClass set. Run Reclassify (17) first.');", | |
| " }", | |
| "}" | |
| ], | |
| "type": "text/javascript" | |
| } | |
| } | |
| ], | |
| "request": { | |
| "method": "GET", | |
| "header": [ | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{adminToken}}" | |
| } | |
| ], | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/diseases/?search={{newDiseaseClass}}", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "diseases", | |
| "" | |
| ], | |
| "query": [ | |
| { | |
| "key": "search", | |
| "value": "{{newDiseaseClass}}" | |
| } | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "18. Admin \u2014 Auto-Verify High Confidence", | |
| "description": "Auto-verifies all unverified predictions with confidence >= threshold (default 90%).\nOnly applies to predictions whose class already exists in the Disease table.\nSkips Unknown predictions.", | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Content-Type", | |
| "value": "application/json" | |
| }, | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{adminToken}}" | |
| } | |
| ], | |
| "body": { | |
| "mode": "raw", | |
| "raw": "{\n \"action\": \"auto_verify\",\n \"threshold\": 0.9\n}" | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/admin/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "admin", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "19. Admin \u2014 Trigger Model Retraining (Train Model Test)", | |
| "description": "Starts model retraining in the background (async). Requires at least 20 images per class in verified_dataset/ unless 'force' is set to 'true'.\n\n## Model Training Test\nThis triggers retrain_pipeline.py which:\n1. Scans verified_dataset/ folders to auto-detect classes\n2. Rebuilds the output layer with the correct number of classes\n3. Fine-tunes the model with data augmentation\n4. Saves the updated model and class names\n5. Recomputes centroids for novelty detection\n\nCheck retrain_status.json in /media/ or run Dashboard Stats (#12) to see training status.", | |
| "request": { | |
| "method": "POST", | |
| "header": [ | |
| { | |
| "key": "Content-Type", | |
| "value": "application/json" | |
| }, | |
| { | |
| "key": "Authorization", | |
| "value": "Bearer {{adminToken}}" | |
| } | |
| ], | |
| "body": { | |
| "mode": "raw", | |
| "raw": "{\n \"action\": \"retrain\",\n \"force\": \"true\"\n}" | |
| }, | |
| "url": { | |
| "raw": "{{baseUrl}}/api/v1/admin/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "v1", | |
| "admin", | |
| "" | |
| ] | |
| } | |
| } | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "API Docs", | |
| "item": [ | |
| { | |
| "name": "20. Swagger UI", | |
| "request": { | |
| "method": "GET", | |
| "url": { | |
| "raw": "{{baseUrl}}/api/docs/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "docs", | |
| "" | |
| ] | |
| } | |
| } | |
| }, | |
| { | |
| "name": "21. OpenAPI Schema (JSON)", | |
| "request": { | |
| "method": "GET", | |
| "url": { | |
| "raw": "{{baseUrl}}/api/schema/", | |
| "host": [ | |
| "{{baseUrl}}" | |
| ], | |
| "path": [ | |
| "api", | |
| "schema", | |
| "" | |
| ] | |
| } | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| } |