add changes

This commit is contained in:
AD2025
2025-11-11 00:25:50 +02:00
commit e3ca132c5e
86 changed files with 22238 additions and 0 deletions

274
backend/models/Category.js Normal file
View File

@@ -0,0 +1,274 @@
const { v4: uuidv4 } = require('uuid');
module.exports = (sequelize, DataTypes) => {
const Category = sequelize.define('Category', {
id: {
type: DataTypes.CHAR(36),
primaryKey: true,
defaultValue: () => uuidv4(),
allowNull: false,
comment: 'UUID primary key'
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
unique: {
msg: 'Category name already exists'
},
validate: {
notEmpty: {
msg: 'Category name cannot be empty'
},
len: {
args: [2, 100],
msg: 'Category name must be between 2 and 100 characters'
}
},
comment: 'Category name'
},
slug: {
type: DataTypes.STRING(100),
allowNull: false,
unique: {
msg: 'Category slug already exists'
},
validate: {
notEmpty: {
msg: 'Slug cannot be empty'
},
is: {
args: /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
msg: 'Slug must be lowercase alphanumeric with hyphens only'
}
},
comment: 'URL-friendly slug'
},
description: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Category description'
},
icon: {
type: DataTypes.STRING(255),
allowNull: true,
comment: 'Icon URL or class'
},
color: {
type: DataTypes.STRING(20),
allowNull: true,
validate: {
is: {
args: /^#[0-9A-F]{6}$/i,
msg: 'Color must be a valid hex color (e.g., #FF5733)'
}
},
comment: 'Display color (hex format)'
},
isActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
field: 'is_active',
comment: 'Category active status'
},
guestAccessible: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'guest_accessible',
comment: 'Whether guests can access this category'
},
// Statistics
questionCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'question_count',
validate: {
min: 0
},
comment: 'Total number of questions in this category'
},
quizCount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'quiz_count',
validate: {
min: 0
},
comment: 'Total number of quizzes taken in this category'
},
// Display order
displayOrder: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'display_order',
comment: 'Display order (lower numbers first)'
}
}, {
sequelize,
modelName: 'Category',
tableName: 'categories',
timestamps: true,
underscored: true,
indexes: [
{
unique: true,
fields: ['name']
},
{
unique: true,
fields: ['slug']
},
{
fields: ['is_active']
},
{
fields: ['guest_accessible']
},
{
fields: ['display_order']
},
{
fields: ['is_active', 'guest_accessible']
}
]
});
// Helper function to generate slug from name
Category.generateSlug = function(name) {
return name
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphens
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
};
// Instance methods
Category.prototype.incrementQuestionCount = async function() {
this.questionCount += 1;
await this.save();
};
Category.prototype.decrementQuestionCount = async function() {
if (this.questionCount > 0) {
this.questionCount -= 1;
await this.save();
}
};
Category.prototype.incrementQuizCount = async function() {
this.quizCount += 1;
await this.save();
};
// Class methods
Category.findActiveCategories = async function(includeGuestOnly = false) {
const where = { isActive: true };
if (includeGuestOnly) {
where.guestAccessible = true;
}
return await this.findAll({
where,
order: [['displayOrder', 'ASC'], ['name', 'ASC']]
});
};
Category.findBySlug = async function(slug) {
return await this.findOne({
where: { slug, isActive: true }
});
};
Category.getGuestAccessibleCategories = async function() {
return await this.findAll({
where: {
isActive: true,
guestAccessible: true
},
order: [['displayOrder', 'ASC'], ['name', 'ASC']]
});
};
Category.getCategoriesWithStats = async function() {
return await this.findAll({
where: { isActive: true },
attributes: [
'id',
'name',
'slug',
'description',
'icon',
'color',
'questionCount',
'quizCount',
'guestAccessible',
'displayOrder'
],
order: [['displayOrder', 'ASC'], ['name', 'ASC']]
});
};
// Hooks
Category.beforeValidate((category) => {
// Auto-generate slug from name if not provided
if (!category.slug && category.name) {
category.slug = Category.generateSlug(category.name);
}
// Ensure UUID is set
if (!category.id) {
category.id = uuidv4();
}
});
Category.beforeCreate((category) => {
// Ensure slug is generated even if validation was skipped
if (!category.slug && category.name) {
category.slug = Category.generateSlug(category.name);
}
});
Category.beforeUpdate((category) => {
// Regenerate slug if name changed
if (category.changed('name') && !category.changed('slug')) {
category.slug = Category.generateSlug(category.name);
}
});
// Define associations
Category.associate = function(models) {
// Category has many questions
if (models.Question) {
Category.hasMany(models.Question, {
foreignKey: 'categoryId',
as: 'questions'
});
}
// Category has many quiz sessions
if (models.QuizSession) {
Category.hasMany(models.QuizSession, {
foreignKey: 'categoryId',
as: 'quizSessions'
});
}
// Category belongs to many guest settings (for guest-accessible categories)
if (models.GuestSettings) {
Category.belongsToMany(models.GuestSettings, {
through: 'guest_settings_categories',
foreignKey: 'categoryId',
otherKey: 'guestSettingsId',
as: 'guestSettings'
});
}
};
return Category;
};