Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,4 @@ REFRESH_SECRET=2B8s85QXmXvCK7RMi7R08iX0
EMAIL_PASSWORD=5ioVUFj4KdOF1aR15iLS9HvZ
CLOUDINARY_CLOUD_NAME=Xl9q9TVz88jF8gxrb9XudOX2
CLOUDINARY_API_KEY=Jfu39X7wCmS05aBd2oJb9j3c
CLOUDINARY_API_SECRET=7W8b0ksC84cQq9n97tXqu8gW




CLOUDINARY_API_SECRET=7W8b0ksC84cQq9n97tXqu8gW
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Add scheduled_time/recurrence to meetings, deadline to challenges

Revision ID: c1d2e3f4a5b6
Revises: e89564e882b1
Create Date: 2026-03-31 10:00:00.000000

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'c1d2e3f4a5b6'
down_revision = 'e89564e882b1'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column(
'weekly_meetings',
sa.Column('scheduled_time', sa.TIMESTAMP(timezone=True), nullable=True)
)
op.add_column(
'weekly_meetings',
sa.Column('recurrence', sa.String(), nullable=True)
)
op.add_column(
'coding_challenges',
sa.Column('deadline', sa.TIMESTAMP(timezone=True), nullable=True)
)


def downgrade() -> None:
op.drop_column('weekly_meetings', 'scheduled_time')
op.drop_column('weekly_meetings', 'recurrence')
op.drop_column('coding_challenges', 'deadline')
79 changes: 79 additions & 0 deletions Alembic/versions/e89564e882b1_add_missing_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Add missing tables (email_templates, weekly_meetings, coding_challenges)

Revision ID: e89564e882b1
Revises: b2c3d4e5f6a7
Create Date: 2026-03-31 16:29:58.728294

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'e89564e882b1'
down_revision = 'b2c3d4e5f6a7'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### Create email_templates table ###
op.create_table('email_templates',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('template_name', sa.String(), nullable=True),
sa.Column('html_content', sa.String(), nullable=True),
sa.Column('subject', sa.String(), nullable=True),
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_email_templates_id'), 'email_templates', ['id'], unique=False)
op.create_index(op.f('ix_email_templates_template_name'), 'email_templates', ['template_name'], unique=True)

# ### Create weekly_meetings table ###
op.create_table('weekly_meetings',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('meeting_url', sa.String(), nullable=False),
sa.Column('description', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False),
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_weekly_meetings_id'), 'weekly_meetings', ['id'], unique=False)

# ### Create coding_challenges table ###
op.create_table('coding_challenges',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('description', sa.String(), nullable=False),
sa.Column('challenge_type', sa.Enum('LEETCODE', 'SYSTEM_DESIGN', 'GENERAL', name='challengetype'), nullable=False),
sa.Column('difficulty', sa.String(), nullable=True),
sa.Column('challenge_url', sa.String(), nullable=True),
sa.Column('posted_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_coding_challenges_id'), 'coding_challenges', ['id'], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
# ### Drop coding_challenges table ###
op.drop_index(op.f('ix_coding_challenges_id'), table_name='coding_challenges')
op.drop_table('coding_challenges')
op.execute('DROP TYPE IF EXISTS challengetype')

# ### Drop weekly_meetings table ###
op.drop_index(op.f('ix_weekly_meetings_id'), table_name='weekly_meetings')
op.drop_table('weekly_meetings')

# ### Drop email_templates table ###
op.drop_index(op.f('ix_email_templates_template_name'), table_name='email_templates')
op.drop_index(op.f('ix_email_templates_id'), table_name='email_templates')
op.drop_table('email_templates')
# ### end Alembic commands ###
144 changes: 112 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
</p>

## 📝 Table of Contents
- [TODO](#todo)
- [About](#about)
- [Getting Started](#getting_started)
- [Running the tests](#tests)
Expand All @@ -27,11 +26,34 @@
- [Built Using](#built_using)
- [Team](#team)

## Todo <a name = "todo"></a>
See [TODO](./docs/TODO.md)

## About <a name = "about"></a>
This project is a CRM API for Slightly Techie. It is built using [these](#built_using) technologies.
The Slightly Techie CRM API is a comprehensive backend service for managing customer relationships, user profiles, projects, skills, announcements, and more. It provides RESTful endpoints for all CRM operations. and includes features like:

Built with FastAPI for high performance and automatic API documentation.

### Automatic Initialization on Startup

When the application starts, it automatically initializes essential seed data:

**Roles**
- Admin
- User
- Guest

**Stacks (Technical Specializations)**
- Backend
- Frontend
- Fullstack
- Mobile
- UI/UX
- DevOps
- Data Science

**Signup Endpoint**
- Created automatically for user registration tracking

This ensures the application is ready to use without manual database setup for these core entities.

## 🏁 Getting Started <a name = "getting_started"></a>
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
Expand Down Expand Up @@ -101,32 +123,49 @@ pip install <package-name>
#### Step 5: Create a `.env` file in the project's root directory and add the following environment variables, replacing the placeholders with your specific values:

```bash
POSTGRES_USER= #e.g postgres
POSTGRES_PASSWORD= #e.g password123
POSTGRES_SERVER= #e.g localhost
POSTGRES_PORT= #e.g 5432
POSTGRES_DB= #e.g st_crm_db
SECRET= #notasecretkey
BASE_URL= #http://127.0.0.1:8080/
# Database Configuration
POSTGRES_USER=postgres # e.g., postgres
POSTGRES_PASSWORD=password123 # e.g., your_secure_password
POSTGRES_SERVER=localhost # e.g., localhost or your_db_host
POSTGRES_PORT=5432 # Default PostgreSQL port
POSTGRES_DB=st_crm_db # e.g., st_crm_db
POSTGRES_DB_TEST=st_crm_db_test # Test database

# Security & Authentication
SECRET=your_secret_key_here # JWT secret key (use a strong value)
REFRESH_SECRET=your_refresh_key # JWT refresh secret key (use a strong value)

# Server Configuration
BASE_URL=http://localhost:3000 # Frontend URL for CORS and links
URL_PATH=/users/reset-password/new-password

# Email Configuration (Gmail SMTP)
EMAIL_SENDER=someawesomeemail@gmail.com
EMAIL_PASSWORD=xxxxxxxxxxx
EMAIL_PASSWORD=your_app_password # Use Gmail app-specific password
EMAIL_SERVER=smtp.gmail.com
EMAIL_PORT=587
AWS_ACCESS_KEY=xxxxxxxxxxxxx
AWS_SECRET_KEY=xxxxxxxxxxx
AWS_BUCKET_NAME=stncrmutilities
AWS_REGION=eu-west-1
URL_PATH=/users/reset-password/new-password

# Cloudinary Configuration (Media Storage)
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret
```

#### Step 6: Create a `test.env` file in the root directory and add the following environment variables

```bash
POSTGRES_USER= #e.g postgres
POSTGRES_PASSWORD= #e.g password123
POSTGRES_SERVER= #e.g localhost
POSTGRES_PORT= #e.g 5432
POSTGRES_DB= #e.g st_crm_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=st_crm_db_test

# Optional: Add these if running tests that involve media uploads
SECRET=test_secret_key
REFRESH_SECRET=test_refresh_secret
CLOUDINARY_CLOUD_NAME=your_test_cloud_name
CLOUDINARY_API_KEY=your_test_api_key
CLOUDINARY_API_SECRET=your_test_api_secret
```

#### Step 7: Start the uvicorn server
Expand All @@ -135,7 +174,50 @@ POSTGRES_DB= #e.g st_crm_db
uvicorn app:app --reload
```

### Step 8: Interact with the Database
> **⚠️ Important - First Time Setup**:
> On the **first startup**, the application automatically initializes seed data:
> - **Roles**: Admin, User, Guest
> - **Stacks**: Backend, Frontend, Fullstack, Mobile, UI/UX, DevOps, Data Science
> - **Signup Endpoint**: Required for user registration
>
> This is handled by the `startup_event()` in `app.py` which is enabled via:
> ```python
> app.add_event_handler("startup", startup_event)
> ```
>
> If this event handler is commented out and you need to re-initialize the database, you can:
> - Uncomment `app.add_event_handler("startup", startup_event)` in `app.py`, OR
> - Run manually in Python:
> ```python
> from db.database import create_roles, create_stacks
> create_roles()
> create_stacks()
> ```

### Step 8: (Optional) Populate Test Data and Update Users

To create 20 dummy users for testing:

```bash
python create_dummy_users.py
```

This creates test users with credentials:
- Email: `user1@slightlytechie.com` → `user20@slightlytechie.com`
- Password: `TestPassword123!` (for all)
- Users are distributed across different stacks

**Additional user management scripts** (if needed):

```bash
# Update all users' status from TO_CONTACT to ACCEPTED
python update_users_status.py

# Update all users' role_id to 2 (User role)
python update_users_role.py
```

### Step 9: Interact with the Database
To interact with the database, you can use tools like psql, pgAdmin or any database client that supports PostgreSQL. Here are some basic commands:

- Connect to the database:
Expand Down Expand Up @@ -239,9 +321,6 @@ pytest
│ │ users.py
│ └───repository
│ users.py
├───docs
│ TODO.md
├───test
│ │ conftest.py
│ │ test_announcements.py
Expand Down Expand Up @@ -299,13 +378,14 @@ visit the API Documentation at [https://crm-api.fly.dev/docs](https://crm-api.fl

## ⛏️ Built Using <a name = "built_using"></a>
- [FastAPI](https://fastapi.tiangolo.com/) - Python Framework
- [Postgres](https://www.postgresql.org/) - Database
- [Fly.io](https://fly.io/) - Cloud Hosting
- [Poetry](https://python-poetry.org/) - Python Package Manager
- [Docker](https://www.docker.com/) - Containerization
- [SqlAlchemy](https://www.sqlalchemy.org/) - ORM
- [Alembic](https://alembic.sqlalchemy.org/en/latest/) - Database Migration
- [PostgreSQL](https://www.postgresql.org/) - Relational Database
- [SQLAlchemy](https://www.sqlalchemy.org/) - ORM (Object-Relational Mapping)
- [Alembic](https://alembic.sqlalchemy.org/en/latest/) - Database Migration Tool
- [Cloudinary](https://cloudinary.com/) - Media Storage & CDN
- [Pytest](https://docs.pytest.org/en/6.2.x/) - Testing Framework
- [Poetry](https://python-poetry.org/) - Python Dependency Manager
- [Docker](https://www.docker.com/) - Containerization
- [Fly.io](https://fly.io/) - Cloud Hosting Platform

## ✍️ Team <a name = "team"></a>
- [@RansfordGenesis](https://github.com/RansfordGenesis)
Expand Down
1 change: 1 addition & 0 deletions api/api_models/announcements.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ class AnnouncementResponse(AnnouncementBase):
class AnnouncementUpdate(BaseModel):
title: Optional[str]
content: Optional[str]
image_url: Optional[str] = None

model_config = ConfigDict(from_attributes=True)
33 changes: 33 additions & 0 deletions api/api_models/coding_challenges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from pydantic import BaseModel, ConfigDict, Field
from datetime import datetime
from typing import Optional
from utils.enums import ChallengeType


class CodingChallengeBase(BaseModel):
title: str = Field(..., min_length=1, max_length=300)
description: str = Field(..., min_length=1)
challenge_type: ChallengeType
difficulty: Optional[str] = Field(None, pattern="^(Easy|Medium|Hard)$")
challenge_url: Optional[str] = None
deadline: Optional[datetime] = None


class CodingChallengeCreate(CodingChallengeBase):
pass


class CodingChallengeUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=300)
description: Optional[str] = Field(None, min_length=1)
challenge_type: Optional[ChallengeType] = None
difficulty: Optional[str] = Field(None, pattern="^(Easy|Medium|Hard)$")
challenge_url: Optional[str] = None
deadline: Optional[datetime] = None


class CodingChallengeResponse(CodingChallengeBase):
id: int
posted_at: datetime
created_by: int
model_config = ConfigDict(from_attributes=True)
Loading
Loading