Import CSV files with Spells into gsm_spells
This commit is contained in:
+71
-30
@@ -90,48 +90,89 @@ func isValidFileType(filename string) bool {
|
||||
// @Summary Import spells from CSV file
|
||||
// @Description Imports spell data from a CSV file into the database. The CSV file should contain spell information with headers matching the database fields.
|
||||
// @Tags importer
|
||||
// @Accept json
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file query string true "Path to the CSV file to import"
|
||||
// @Param file formData file true "CSV file to import"
|
||||
// @Success 200 {object} map[string]interface{} "Import successful"
|
||||
// @Failure 400 {object} map[string]interface{} "Bad request - missing file parameter, file not found, or invalid file type"
|
||||
// @Failure 500 {object} map[string]interface{} "Internal server error - import failed"
|
||||
// @Router /api/importer/spells/csv [post]
|
||||
func ImportSpellCSVHandler(c *gin.Context) {
|
||||
// Get the file path from query parameter
|
||||
filePath := c.Query("file")
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Missing file parameter",
|
||||
"message": "Please provide a file path using the 'file' query parameter",
|
||||
})
|
||||
return
|
||||
// Try to get file from multipart form first
|
||||
file, err := c.FormFile("file")
|
||||
var filePath string
|
||||
|
||||
if err != nil {
|
||||
// Fallback to query parameter for backward compatibility
|
||||
filePath = c.Query("file")
|
||||
if filePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Missing file parameter",
|
||||
"message": "Please provide a CSV file via multipart upload or file path using the 'file' parameter",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate file exists and has proper extension
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "File not found",
|
||||
"message": fmt.Sprintf("File '%s' does not exist", filePath),
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Handle uploaded file
|
||||
// Check file extension
|
||||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||
if ext != ".csv" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid file type",
|
||||
"message": "Only CSV files are supported",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create uploads directory if it doesn't exist
|
||||
uploadDir := "./uploads"
|
||||
if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to create upload directory",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Save the uploaded file
|
||||
filePath = filepath.Join(uploadDir, file.Filename)
|
||||
if err := c.SaveUploadedFile(file, filePath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to save uploaded file",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate file exists and has proper extension
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "File not found",
|
||||
"message": fmt.Sprintf("File '%s' does not exist", filePath),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
if ext != ".csv" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid file type",
|
||||
"message": "Only CSV files are supported",
|
||||
})
|
||||
return
|
||||
// Check file extension for query parameter path
|
||||
if file == nil {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
if ext != ".csv" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid file type",
|
||||
"message": "Only CSV files are supported",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Clear source cache before import to ensure fresh data
|
||||
ClearSourceCache()
|
||||
|
||||
// Perform the import
|
||||
err := ImportCsv2Spell(filePath)
|
||||
err = ImportCsv2Spell(filePath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Import failed",
|
||||
@@ -147,7 +188,7 @@ func ImportSpellCSVHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Spells imported successfully",
|
||||
"file": filePath,
|
||||
"file": filepath.Base(filePath),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -155,7 +196,7 @@ func ImportSpellCSVHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Spells imported successfully",
|
||||
"file": filePath,
|
||||
"file": filepath.Base(filePath),
|
||||
"total_spells": spellCount,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ package importer
|
||||
import (
|
||||
"bamort/database"
|
||||
"bamort/models"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -317,4 +320,66 @@ Test Spell HTTP,ARK,3,Beherrschen`
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
assert.Equal(t, "Invalid file type", response["error"], "Should return correct error")
|
||||
})
|
||||
|
||||
t.Run("File upload with multipart form", func(t *testing.T) {
|
||||
// Setup test database
|
||||
database.SetupTestDB(true, false)
|
||||
models.MigrateStructure()
|
||||
|
||||
// Create test CSV content
|
||||
csvContent := `name,beschreibung,quelle,stufe,ap
|
||||
Test Spell Upload,Test description,ARK,1,2`
|
||||
|
||||
// Create temporary file
|
||||
tmpFile, err := os.CreateTemp("", "test_spell_upload_*.csv")
|
||||
assert.NoError(t, err, "Should create temp file")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
_, err = tmpFile.WriteString(csvContent)
|
||||
assert.NoError(t, err, "Should write CSV content")
|
||||
tmpFile.Close()
|
||||
|
||||
// Create multipart form data
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// Add file field
|
||||
file, err := os.Open(tmpFile.Name())
|
||||
assert.NoError(t, err, "Should open temp file")
|
||||
defer file.Close()
|
||||
|
||||
part, err := writer.CreateFormFile("file", "test_spells.csv")
|
||||
assert.NoError(t, err, "Should create form file")
|
||||
|
||||
_, err = io.Copy(part, file)
|
||||
assert.NoError(t, err, "Should copy file content")
|
||||
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err, "Should close writer")
|
||||
|
||||
// Create request
|
||||
router := gin.New()
|
||||
router.POST("/test", ImportSpellCSVHandler)
|
||||
|
||||
req, err := http.NewRequest("POST", "/test", body)
|
||||
assert.NoError(t, err, "Should create request")
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code, "Should return 200 OK")
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err, "Should parse JSON response")
|
||||
assert.True(t, response["success"].(bool), "Should be successful")
|
||||
assert.Contains(t, response["message"], "imported successfully", "Should contain success message")
|
||||
|
||||
// Verify spell was imported
|
||||
var spell models.Spell
|
||||
err = database.DB.Where("name = ?", "Test Spell Upload").First(&spell).Error
|
||||
assert.NoError(t, err, "Should find imported spell")
|
||||
assert.Equal(t, "Test description", spell.Beschreibung, "Should have correct description")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
<template>
|
||||
<div class="header-section">
|
||||
<h2>{{ $t('maintenance') }}</h2>
|
||||
<!-- Add search input -->
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchTerm"
|
||||
:placeholder="`${$t('search')} ${$t('Spell')}...`"
|
||||
/>
|
||||
</div>
|
||||
<!-- Add search input -->
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchTerm"
|
||||
:placeholder="`${$t('search')} ${$t('Spell')}...`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import CSV Section -->
|
||||
<div class="import-section">
|
||||
<h3>{{ $t('spell.import') || 'Import Spells' }}</h3>
|
||||
<div class="file-upload">
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".csv"
|
||||
@change="handleFileSelect"
|
||||
style="display: none;"
|
||||
/>
|
||||
<button
|
||||
@click="$refs.fileInput.click()"
|
||||
:disabled="importing"
|
||||
class="upload-btn"
|
||||
>
|
||||
{{ importing ? ($t('spell.importing') || 'Importing...') : ($t('spell.selectCsv') || 'Select CSV File') }}
|
||||
</button>
|
||||
<span v-if="selectedFile" class="file-name">{{ selectedFile.name }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="selectedFile && !importing"
|
||||
@click="importSpells"
|
||||
class="import-btn"
|
||||
>
|
||||
{{ $t('spell.import') || 'Import Spells' }}
|
||||
</button>
|
||||
<div v-if="importResult" class="import-result" :class="importResult.success ? 'success' : 'error'">
|
||||
{{ importResult.message }}
|
||||
<span v-if="importResult.total_spells"> ({{ importResult.total_spells }} spells total)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cd-view">
|
||||
<div class="cd-list">
|
||||
@@ -95,19 +128,85 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3rem;
|
||||
margin-bottom: 1rem;
|
||||
height: fit-content;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 0.2rem;
|
||||
width: 200px;
|
||||
padding: 0.5rem;
|
||||
width: 250px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.import-section {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
border: 2px solid #1da766;
|
||||
border-radius: 8px;
|
||||
background-color: #f8fcfa;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.import-section h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.upload-btn, .import-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: #1da766;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.upload-btn:hover, .import-btn:hover {
|
||||
background-color: #166d4a;
|
||||
}
|
||||
|
||||
.upload-btn:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.import-result {
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.import-result.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.import-result.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.tables-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@@ -153,7 +252,10 @@ export default {
|
||||
sortField: 'name',
|
||||
sortAsc: true,
|
||||
editingIndex: -1,
|
||||
editedItem: null
|
||||
editedItem: null,
|
||||
selectedFile: null,
|
||||
importing: false,
|
||||
importResult: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -228,7 +330,77 @@ export default {
|
||||
} catch (error) {
|
||||
console.error('Failed to update spell:', error);
|
||||
}
|
||||
},
|
||||
handleFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file && file.type === 'text/csv') {
|
||||
this.selectedFile = file;
|
||||
this.importResult = null;
|
||||
} else {
|
||||
this.selectedFile = null;
|
||||
this.importResult = {
|
||||
success: false,
|
||||
message: 'Please select a valid CSV file.'
|
||||
};
|
||||
}
|
||||
},
|
||||
async importSpells() {
|
||||
if (!this.selectedFile) {
|
||||
this.importResult = {
|
||||
success: false,
|
||||
message: 'Please select a CSV file first.'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this.importing = true;
|
||||
this.importResult = null;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.selectedFile);
|
||||
|
||||
const response = await API.post('/api/importer/spells/csv', formData, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
this.importResult = {
|
||||
success: true,
|
||||
message: response.data.message,
|
||||
total_spells: response.data.total_spells
|
||||
};
|
||||
|
||||
// Refresh the spells data after successful import
|
||||
await this.refreshSpellsData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to import spells:', error);
|
||||
this.importResult = {
|
||||
success: false,
|
||||
message: error.response?.data?.message || 'Import failed. Please try again.'
|
||||
};
|
||||
} finally {
|
||||
this.importing = false;
|
||||
}
|
||||
},
|
||||
async refreshSpellsData() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await API.get('/api/maintenance', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
// Update the spells data
|
||||
if (response.data.spells) {
|
||||
this.mdata.spells = response.data.spells;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh spells data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user