Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.

Commit 90d9c49

Browse files
authored
Merge pull request #33 from paiindustries/feature/testimonial
Implemented testimonial model and migration
2 parents af00e40 + 5d11c39 commit 90d9c49

3 files changed

Lines changed: 296 additions & 2 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
component extends="wheels.migrator.Migration" hint="creates testimonials table and adds testimonial flag to users" {
2+
3+
function up() {
4+
transaction {
5+
try {
6+
// Add has_testimonial column to users table
7+
addColumn(
8+
table = "users",
9+
columnType = "boolean",
10+
columnName = "has_testimonial",
11+
default = false,
12+
null = false
13+
);
14+
15+
// Create testimonials table
16+
t = createTable(name = 'testimonials', force=false, id=true, primaryKey='id');
17+
t.integer(columnNames='user_id', null=false);
18+
t.string(columnNames='company_name', null=false, default='', limit=255);
19+
t.string(columnNames='logo_path', null=true, limit=255);
20+
t.string(columnNames='experience_level', null=false, default='Beginner', limit=50);
21+
t.text(columnNames='testimonial_text', null=false);
22+
t.integer(columnNames='rating', null=false, default=0);
23+
t.boolean(columnNames='display_permission', null=false, default=false);
24+
t.string(columnNames='social_media_links', null=true, limit=500);
25+
t.string(columnNames='website_url', null=true, limit=255);
26+
t.boolean(columnNames='is_featured', null=false, default=false);
27+
t.boolean(columnNames='is_approved', null=false, default=false);
28+
t.timestamps();
29+
t.create();
30+
31+
// Add unique index on user_id
32+
addIndex(
33+
table = "testimonials",
34+
columnNames = "user_id",
35+
unique = true
36+
);
37+
38+
// Add foreign key constraint
39+
addForeignKey(
40+
table = 'testimonials',
41+
column = 'user_id',
42+
referenceTable = 'users',
43+
referenceColumn = 'id'
44+
);
45+
46+
} catch (any ex) {
47+
local.exception = ex;
48+
}
49+
50+
if (StructKeyExists(local, "exception")) {
51+
transaction action="rollback";
52+
Throw(errorCode = "1", detail = local.exception.detail, message = local.exception.message, type = "any");
53+
} else {
54+
transaction action="commit";
55+
}
56+
}
57+
}
58+
59+
function down() {
60+
transaction {
61+
try {
62+
// Drop foreign key constraints using raw SQL
63+
execute(sql="ALTER TABLE testimonials DROP CONSTRAINT IF EXISTS fk_testimonials_user_id");
64+
65+
// Drop testimonials table
66+
dropTable(name = 'testimonials');
67+
68+
// Remove has_testimonial column from users table
69+
removeColumn(table="users", columnName="has_testimonial");
70+
71+
} catch (any ex) {
72+
local.exception = ex;
73+
}
74+
75+
if (StructKeyExists(local, "exception")) {
76+
transaction action="rollback";
77+
Throw(errorCode = "1", detail = local.exception.detail, message = local.exception.message, type = "any");
78+
} else {
79+
transaction action="commit";
80+
}
81+
}
82+
}
83+
}

app/models/Testimonial.cfc

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
component extends="app.Models.Model" {
2+
function config() {
3+
table("testimonials");
4+
5+
// ID Property
6+
property(
7+
name="id",
8+
column="id",
9+
dataType="integer",
10+
automaticValidations=false
11+
);
12+
13+
// User ID Property (Foreign Key)
14+
property(
15+
name="userId",
16+
column="user_id",
17+
dataType="integer",
18+
label="User ID",
19+
automaticValidations=true
20+
);
21+
22+
// Company Name Property
23+
property(
24+
name="companyName",
25+
column="company_name",
26+
dataType="string",
27+
label="Company Name",
28+
defaultValue=""
29+
);
30+
31+
// Logo Path Property
32+
property(
33+
name="logoPath",
34+
column="logo_path",
35+
dataType="string",
36+
label="Logo Path",
37+
defaultValue=""
38+
);
39+
40+
// Experience Level Property
41+
property(
42+
name="experienceLevel",
43+
column="experience_level",
44+
dataType="string",
45+
label="Experience Level",
46+
defaultValue="Beginner"
47+
);
48+
49+
// Testimonial Text Property
50+
property(
51+
name="testimonialText",
52+
column="testimonial_text",
53+
dataType="text",
54+
label="Testimonial Text",
55+
defaultValue=""
56+
);
57+
58+
// Rating Property
59+
property(
60+
name="rating",
61+
column="rating",
62+
dataType="integer",
63+
label="Rating",
64+
defaultValue=0
65+
);
66+
67+
// Display Permission Property
68+
property(
69+
name="displayPermission",
70+
column="display_permission",
71+
dataType="boolean",
72+
label="Display Permission",
73+
defaultValue=false
74+
);
75+
76+
// Social Media Links Property
77+
property(
78+
name="socialMediaLinks",
79+
column="social_media_links",
80+
dataType="string",
81+
label="Social Media Links",
82+
defaultValue=""
83+
);
84+
85+
// Website URL Property
86+
property(
87+
name="websiteUrl",
88+
column="website_url",
89+
dataType="string",
90+
label="Website URL",
91+
defaultValue=""
92+
);
93+
94+
// Is Featured Property
95+
property(
96+
name="isFeatured",
97+
column="is_featured",
98+
dataType="boolean",
99+
label="Is Featured",
100+
defaultValue=false
101+
);
102+
103+
// Is Approved Property
104+
property(
105+
name="isApproved",
106+
column="is_approved",
107+
dataType="boolean",
108+
label="Is Approved",
109+
defaultValue=false
110+
);
111+
112+
// Timestamps with custom column names
113+
property(
114+
name="createdAt",
115+
column="createdat",
116+
dataType="timestamp",
117+
label="Created On"
118+
);
119+
120+
property(
121+
name="updatedAt",
122+
column="updatedat",
123+
dataType="timestamp",
124+
label="Last Updated"
125+
);
126+
127+
// Relationships
128+
belongsTo(name="User", foreignKey="userId");
129+
130+
// Validations
131+
validatesPresenceOf(properties="companyName,experienceLevel,testimonialText,rating");
132+
validatesLengthOf(property="testimonialText", minimum=20, maximum=500, message="Testimonial text must be between 20 and 500 characters.");
133+
validatesNumericalityOf(property="rating", minimum=1, maximum=5, message="Rating must be between 1 and 5.");
134+
}
135+
136+
// Get all approved testimonials
137+
public function getApprovedTestimonials(struct options = {}) {
138+
// Default options
139+
local.defaultOptions = {
140+
page = 1,
141+
perPage = 10,
142+
sortBy = "createdAt",
143+
sortOrder = "DESC",
144+
onlyFeatured = false
145+
};
146+
147+
// Merge provided options
148+
local.mergedOptions = structAppend(local.defaultOptions, arguments.options);
149+
150+
// Build where clause
151+
local.whereClause = "isApproved = 1 AND displayPermission = 1";
152+
153+
// Add featured filter if specified
154+
if (local.mergedOptions.onlyFeatured) {
155+
local.whereClause &= " AND isFeatured = 1";
156+
}
157+
158+
// Find testimonials
159+
return findAll(
160+
where = local.whereClause,
161+
order = "#local.mergedOptions.sortBy# #local.mergedOptions.sortOrder#",
162+
page = local.mergedOptions.page,
163+
perPage = local.mergedOptions.perPage,
164+
include = "User"
165+
);
166+
}
167+
168+
// Format testimonial for display
169+
public function formatForDisplay() {
170+
local.user = this.user();
171+
172+
return {
173+
id = this.id,
174+
companyName = this.companyName,
175+
logoPath = this.logoPath,
176+
testimonialText = this.testimonialText,
177+
rating = this.rating,
178+
experienceLevel = this.experienceLevel,
179+
websiteUrl = this.websiteUrl,
180+
socialMediaLinks = this.socialMediaLinks,
181+
userFullName = local.user.fullName,
182+
userProfilePicture = local.user.profilePicture,
183+
isFeatured = this.isFeatured,
184+
submittedOn = this.createdAt
185+
};
186+
}
187+
}

app/models/User.cfc

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ component extends="app.Models.Model" {
5454
select=false // Exclude from default select statements
5555
);
5656

57-
5857
// Profile Picture Property
5958
property(
6059
name="profilePicture",
@@ -83,6 +82,15 @@ component extends="app.Models.Model" {
8382
defaultValue=true
8483
);
8584

85+
// Has Testimonial Property
86+
property(
87+
name="hasTestimonial",
88+
column="has_testimonial",
89+
dataType="boolean",
90+
label="Has Submitted Testimonial",
91+
defaultValue=false
92+
);
93+
8694
// Timestamps with custom column names
8795
property(
8896
name="createdAt",
@@ -115,6 +123,9 @@ component extends="app.Models.Model" {
115123

116124
// Relationships
117125
belongsTo(name="Role", foreignKey="roleId");
126+
127+
// Testimonial Relationship - One user has one testimonial
128+
hasOne(name="Testimonial", foreignKey="userId");
118129
}
119130

120131
// Fetch all users with their roles
@@ -155,7 +166,20 @@ component extends="app.Models.Model" {
155166
fullName = this.fullName,
156167
email = this.email,
157168
profileUrl = this.profileUrl,
158-
profilePicture = this.profilePicture
169+
profilePicture = this.profilePicture,
170+
hasTestimonial = this.hasTestimonial
159171
};
160172
}
173+
174+
// Check if user has submitted a testimonial
175+
public function hasSubmittedTestimonial() {
176+
testimonial = model("Testimonial").findOne(where="userId=#this.id#");
177+
return IsObject(testimonial);
178+
}
179+
180+
// Update testimonial status after submission
181+
public function markTestimonialSubmitted() {
182+
this.hasTestimonial = true;
183+
return this.save(validate=false);
184+
}
161185
}

0 commit comments

Comments
 (0)