diff --git a/README.md b/README.md index 9b7ad0691a..6a26d22970 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,29 @@ - 🎮 **Gamification** - Leaderboards, challenges, and competitions to make security research engaging - 💰 **Staking System** - Innovative blockchain-based reward system for contributors - 📊 **Comprehensive Dashboard** - Track your progress, statistics, and impact +- 🔥 **Sizzle Plugin** - Daily check-ins, time tracking, and team productivity tools ([Learn more](sizzle/README.md)) - 🌐 **Open Source** - Built with transparency and collaboration at its core - 🛡️ **OWASP Project** - Part of the Open Worldwide Application Security Project family --- -## 🚀 Quick Start +## � Featured Plugins + +BLT includes powerful plugins to enhance team productivity and collaboration: + +### Sizzle - Daily Check-ins & Time Tracking +A comprehensive productivity plugin featuring: +- 📝 **Daily Status Reports** - Structured team check-ins and progress tracking +- ⏰ **Time Tracking** - Log work sessions with GitHub issue integration +- 🔔 **Smart Reminders** - Configurable daily reminder notifications +- 📊 **Team Analytics** - Monitor productivity and engagement metrics +- 🎯 **Streak Tracking** - Maintain daily check-in streaks for motivation + +**📖 [View Sizzle Documentation](sizzle/README.md)** | **🌐 Access at**: `/sizzle/` + +--- + +## �🚀 Quick Start ### Prerequisites - Python 3.11.2+ @@ -100,6 +117,8 @@ docker-compose up Access the application at **http://localhost:8000** +**🔥 Sizzle Plugin**: Access the productivity tools at **http://localhost:8000/sizzle/** + #### Using Poetry ```bash # Install dependencies diff --git a/blt/settings.py b/blt/settings.py index 20b36ecb09..05e249af66 100644 --- a/blt/settings.py +++ b/blt/settings.py @@ -101,6 +101,7 @@ "dj_rest_auth.registration", "storages", "channels", + "sizzle", ) if DEBUG: @@ -176,6 +177,8 @@ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "django.template.context_processors.i18n", + # Add sizzle context processor for seamless BLT integration + "sizzle.context_processors.sizzle_context", ], "loaders": ( [ @@ -630,3 +633,18 @@ } THROTTLE_WINDOW = 60 # 60 seconds (1 minute) THROTTLE_EXEMPT_PATHS = ["/admin/", "/static/", "/media/"] + +# Sizzle Configuration - Integrate with BLT's layout and navigation +SIZZLE_PARENT_BASE = "base.html" # Use BLT's main template +SIZZLE_SHOW_SIDENAV = True # Show BLT's sidenav in sizzle pages + +# Sizzle Model Configuration (Swappable Models) +SIZZLE_SLACK_INTEGRATION_MODEL = "website.SlackIntegration" +SIZZLE_ORGANIZATION_MODEL = "website.Organization" +SIZZLE_USERPROFILE_MODEL = "website.UserProfile" +SIZZLE_NOTIFICATION_MODEL = "website.Notification" + +# Sizzle Feature Flags +SIZZLE_SLACK_ENABLED = True +SIZZLE_EMAIL_REMINDERS_ENABLED = True +SIZZLE_DAILY_CHECKINS_ENABLED = True diff --git a/blt/urls.py b/blt/urls.py index 4f5064ef0b..af0f0ee1fa 100644 --- a/blt/urls.py +++ b/blt/urls.py @@ -210,18 +210,13 @@ RoomCreateView, RoomsListView, ScoreboardView, - TimeLogListAPIView, - TimeLogListView, UpcomingHunts, add_domain_to_organization, add_or_update_domain, add_or_update_organization, - add_sizzle_checkIN, admin_organization_dashboard, admin_organization_dashboard_detail, approve_activity, - checkIN, - checkIN_detail, delete_room, delete_time_entry, dislike_activity, @@ -238,15 +233,11 @@ organization_hunt_results, room_messages_api, send_message_api, - sizzle, - sizzle_daily_log, - sizzle_docs, subscribe_to_domains, trademark_detailview, trademark_search, update_organization_repos, update_role, - user_sizzle_report, view_hunt, weekly_report, ) @@ -965,19 +956,7 @@ TagApiViewset.as_view({"get": "list", "post": "create"}), name="tags-api", ), - path("sizzle/", sizzle, name="sizzle"), - path("check-in/", checkIN, name="checkIN"), - path("add-sizzle-checkin/", add_sizzle_checkIN, name="add_sizzle_checkin"), - path("check-in//", checkIN_detail, name="checkIN_detail"), - path("sizzle-docs/", sizzle_docs, name="sizzle-docs"), - path("api/timelogsreport/", TimeLogListAPIView, name="timelogsreport"), - path("time-logs/", TimeLogListView, name="time_logs"), - path("sizzle-daily-log/", sizzle_daily_log, name="sizzle_daily_log"), - path( - "user-sizzle-report//", - user_sizzle_report, - name="user_sizzle_report", - ), + path("sizzle/", include("sizzle.urls")), path("submit-roadmap-pr/", submit_roadmap_pr, name="submit-roadmap-pr"), path("view-pr-analysis/", view_pr_analysis, name="view_pr_analysis"), path("delete_time_entry/", delete_time_entry, name="delete_time_entry"), diff --git a/sizzle/README.md b/sizzle/README.md new file mode 100644 index 0000000000..7df0841ba7 --- /dev/null +++ b/sizzle/README.md @@ -0,0 +1,903 @@ +# Sizzle + +A pluggable Django app for daily check-ins, time tracking, and team activity monitoring. Track your team's progress, manage daily status reports, and visualize productivity with built-in leaderboards and streak tracking. + +## Features + +- **Daily Check-ins**: Team members submit daily status reports +- **Time Logging**: Track time spent on tasks and projects +- **Leaderboards**: Gamified rankings based on activity +- **Streak Tracking**: Monitor consecutive check-in streaks +- **Reminders**: Automated daily reminder system (email/Slack) +- **Analytics**: View individual and team reports +- **Slack Integration**: Post check-ins directly to Slack (optional) + +## Architecture & Design + +Sizzle follows Django's best practices for reusable apps: + +### **🔌 Pluggable Design** +- **Works standalone**: No external dependencies for core features +- **Integrates seamlessly**: Optional integration with existing models +- **Graceful degradation**: Features disable cleanly when dependencies unavailable + +### **⚙️ Configurable Models** +Uses Django's swappable model pattern (like `AUTH_USER_MODEL`): +```python +# Default (works out of the box) +SIZZLE_ORGANIZATION_MODEL = None + +# Integrate with your project +SIZZLE_ORGANIZATION_MODEL = 'your_app.Organization' +``` + +### **🎛️ Feature Flags** +Enable only what you need: +```python +SIZZLE_SLACK_ENABLED = False # Disable Slack +SIZZLE_EMAIL_REMINDERS_ENABLED = True # Keep email reminders +SIZZLE_DAILY_CHECKINS_ENABLED = True # Keep check-ins +``` + +### **🧩 Template Integration** +Automatically integrates with your project's layout: +```python +SIZZLE_PARENT_BASE = 'base.html' # Use your base template +SIZZLE_SHOW_SIDENAV = True # Include your navigation +``` + +## Requirements + +- Python >= 3.8 +- Django >= 4.0 +- pytz + +## Installation + +### 1. Install via pip + +``` +pip install django-sizzle +``` + +Or install from source: +``` +git clone https://github.com/OWASP-BLT/django-sizzle.git +cd django-sizzle +pip install -e . +``` + +### 2. Add to INSTALLED_APPS + +In your Django project's `settings.py`: + +``` +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # Your apps + 'myapp', + + # Add Sizzle + 'sizzle', +] +``` + +### 3. Include Sizzle URLs + +In your main `urls.py`: + +``` +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + + # Add Sizzle URLs + path('sizzle/', include('sizzle.urls')), + + # Your other URLs +] +``` + +### 4. Add Context Processor (For Template Integration) + +Add the context processor to your `settings.py` to enable seamless template integration: + +```python +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], # Your templates directory + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + + # Add Sizzle context processor for template integration + 'sizzle.context_processors.sizzle_context', + ], + }, + }, +] +``` + +### 5. Configure Template Integration (Optional) + +To integrate Sizzle with your project's existing layout: + +```python +# In settings.py - Tell Sizzle which base template to extend from your project +SIZZLE_PARENT_BASE = 'base.html' # Your main project template + +# If your project has sidenav/navigation that Sizzle should include +SIZZLE_SHOW_SIDENAV = True # Default: True +``` + +### 6. Run Migrations + +``` +python manage.py migrate sizzle +``` + +This creates three database tables: +- `sizzle_dailystatusreport`: Stores daily check-in reports +- `sizzle_timelog`: Stores time tracking entries +- `sizzle_remindersettings`: Stores user reminder preferences + +### 7. Collect Static Files + +``` +python manage.py collectstatic +``` + +### 8. Create Superuser (if needed) + +``` +python manage.py createsuperuser +``` + +## Quick Start + +### Access Sizzle + +Start your development server: +``` +python manage.py runserver +``` + +Visit these URLs: +- Main dashboard: `http://localhost:8000/sizzle/` +- Submit check-in: `http://localhost:8000/sizzle/check-in/` +- View time logs: `http://localhost:8000/sizzle/time-logs/` +- Admin panel: `http://localhost:8000/admin/` + +### Submit Your First Check-in + +1. Log in to your Django app +2. Navigate to `/sizzle/check-in/` +3. Fill out the daily status form +4. Submit to track your streak! + +## Configuration + +### Basic Configuration + +Sizzle follows Django's best practices for swappable models, similar to `AUTH_USER_MODEL`. This makes it work with different project structures while providing sensible defaults. + +#### Required Settings + +```python +# settings.py +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # Your apps + 'myapp', + + # Add Sizzle + 'sizzle', +] + +# Template Context Processor (for template integration) +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + + # Add Sizzle context processor + 'sizzle.context_processors.sizzle_context', + ], + }, + }, +] +``` + +### Model Integration (Optional) + +If your project has existing models that Sizzle should integrate with, configure them: + +```python +# settings.py + +# Model Configuration (all optional) +SIZZLE_SLACK_INTEGRATION_MODEL = 'your_app.SlackIntegration' # For Slack notifications +SIZZLE_ORGANIZATION_MODEL = 'your_app.Organization' # For organization features +SIZZLE_USERPROFILE_MODEL = 'your_app.UserProfile' # For user profiles +SIZZLE_NOTIFICATION_MODEL = 'your_app.Notification' # For in-app notifications + +# Feature Flags +SIZZLE_SLACK_ENABLED = True # Enable Slack integration +SIZZLE_EMAIL_REMINDERS_ENABLED = True # Enable email reminders +SIZZLE_DAILY_CHECKINS_ENABLED = True # Enable check-in reminders + +# Template Integration +SIZZLE_PARENT_BASE = 'base.html' # Your project's base template +SIZZLE_SHOW_SIDENAV = True # Show your project's navigation +``` + +### Slack Integration (Optional) + +To enable Slack notifications: + +1. **Install Slack dependencies:** +```bash +pip install slack-bolt +``` + +2. **Create SlackIntegration model in your project:** +```python +# your_app/models.py +class SlackIntegration(models.Model): + integration = models.ForeignKey('Integration', on_delete=models.CASCADE) + bot_access_token = models.CharField(max_length=255) + default_channel_id = models.CharField(max_length=255) + daily_updates = models.BooleanField(default=False) + daily_update_time = models.IntegerField() # Hour in UTC (0-23) +``` + +3. **Configure in settings:** +```python +SIZZLE_SLACK_ENABLED = True +SIZZLE_SLACK_INTEGRATION_MODEL = 'your_app.SlackIntegration' +``` + +4. **Run the command:** +```bash +python manage.py slack_daily_timelogs +``` + +### Model Requirements + +Sizzle works standalone, but integrates better with these optional models: + +**SlackIntegration Model:** +- `bot_access_token` (CharField): Slack bot token +- `default_channel_id` (CharField): Default Slack channel +- `daily_updates` (BooleanField): Enable daily updates +- `daily_update_time` (IntegerField): Hour for updates (UTC) +- `integration.organization` (ForeignKey): Related organization + +**UserProfile Model:** +- `user` (OneToOneField to User): Related user +- `team` (ForeignKey): User's team/organization +- `team.check_ins_enabled` (BooleanField): Enable check-ins +- `last_check_in` (DateField): Last check-in date + +**Notification Model:** +- `user` (ForeignKey to User): Target user +- `message` (TextField): Notification message +- `notification_type` (CharField): Type of notification +- `link` (URLField): Link to action + +**Organization Model:** +- Any model that represents teams/organizations +- Referenced by TimeLog for organization-specific time tracking + +## Configuration Examples + +### Standalone Project (Minimal) + +```python +# settings.py +INSTALLED_APPS = ['sizzle'] + +# All features disabled except core functionality +SIZZLE_SLACK_ENABLED = False +SIZZLE_EMAIL_REMINDERS_ENABLED = True +SIZZLE_DAILY_CHECKINS_ENABLED = False +``` + +### BLT Integration (Full Features) + +```python +# settings.py +INSTALLED_APPS = ['website', 'sizzle'] + +# Model Configuration +SIZZLE_SLACK_INTEGRATION_MODEL = 'website.SlackIntegration' +SIZZLE_ORGANIZATION_MODEL = 'website.Organization' +SIZZLE_USERPROFILE_MODEL = 'website.UserProfile' +SIZZLE_NOTIFICATION_MODEL = 'website.Notification' + +# All features enabled +SIZZLE_SLACK_ENABLED = True +SIZZLE_EMAIL_REMINDERS_ENABLED = True +SIZZLE_DAILY_CHECKINS_ENABLED = True + +# Template Integration +SIZZLE_PARENT_BASE = 'base.html' +SIZZLE_SHOW_SIDENAV = True +``` + +### Custom Project + +```python +# settings.py +# Use your own models +SIZZLE_ORGANIZATION_MODEL = 'companies.Company' +SIZZLE_USERPROFILE_MODEL = 'accounts.Profile' + +# Disable unused features +SIZZLE_SLACK_ENABLED = False +SIZZLE_DAILY_CHECKINS_ENABLED = False + +# Keep email reminders +SIZZLE_EMAIL_REMINDERS_ENABLED = True +``` + +## Quick Start + +### Basic Settings + +Add these to your `settings.py` to customize Sizzle: + +``` +# Optional: Customize the base template (see Template Customization below) +SIZZLE_BASE_TEMPLATE = 'sizzle/base.html' # Default + +# Optional: Sizzle-specific settings +SIZZLE_SETTINGS = { + 'ENABLE_SLACK_INTEGRATION': False, # Set to True if using Slack + 'ENABLE_EMAIL_REMINDERS': True, # Send email reminders + 'DEFAULT_REMINDER_TIME': '09:00', # Daily reminder time (24-hour format) + 'STREAK_TRACKING_ENABLED': True, # Track consecutive check-in streaks + 'TIMEZONE': 'UTC', # Timezone for check-ins +} +``` + +### Slack Integration (Optional) + +If you want Slack notifications: + +1. Install Slack dependencies: +``` +pip install django-sizzle[slack] +``` + +2. Add Slack configuration to `settings.py`: +``` +SIZZLE_SETTINGS = { + 'ENABLE_SLACK_INTEGRATION': True, +} + +# You'll need these from your Slack app +SLACK_BOT_TOKEN = 'xoxb-your-bot-token' +SLACK_SIGNING_SECRET = 'your-signing-secret' +``` + +3. Run the Slack time log command: +``` +python manage.py slack_daily_timelogs +``` + +### Email Reminders + +To enable email reminders for check-ins: + +1. Configure Django email settings in `settings.py`: +``` +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = 'your-email@gmail.com' +EMAIL_HOST_PASSWORD = 'your-app-password' +``` + +2. Set up a cron job to send daily reminders: +``` +# Add to crontab (example: 9 AM daily) +0 9 * * * cd /path/to/project && python manage.py send_daily_reminders +``` + +## Template Customization + +Sizzle provides its own minimal base template (`sizzle/base.html`) so it works out-of-the-box. However, you'll likely want to integrate it with your site's design. + +### Option 1: Override Sizzle's Base Template + +Create `your_project/templates/sizzle/base.html`: + +``` +{% extends "your_site_base.html" %} + +{% block your_content_block %} + {# Sizzle templates expect these blocks: #} + {% block title %}{% endblock %} + {% block content %}{% endblock %} +{% endblock %} + +{% block extra_css %} + {% load static %} + +{% endblock %} +``` + +Make sure your template loader can find it: +``` +# settings.py +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], # Your project templates + 'APP_DIRS': True, # Important: Finds sizzle/templates/ + ... + }, +] +``` + +### Option 2: Use Settings to Specify Base Template + +In `settings.py`: +``` +SIZZLE_BASE_TEMPLATE = 'my_site/base.html' +``` + +Then create `my_site/base.html` with these required blocks: +``` +{% block title %}{% endblock %} +{% block content %}{% endblock %} +``` + +### Required Template Blocks + +All Sizzle templates expect these blocks in your base template: + +- `{% block title %}` - Page title +- `{% block content %}` - Main content area +- `{% block extra_head %}` - Additional CSS/meta tags (optional) +- `{% block extra_js %}` - Additional JavaScript (optional) + +## Database Models + +### DailyStatusReport + +Stores daily check-in submissions. + +**Fields**: +- `user` (ForeignKey): User who submitted the report +- `content` (TextField): Check-in message/status +- `created_at` (DateTimeField): Submission timestamp +- `streak_count` (IntegerField): Current streak days + +**Example usage**: +``` +from sizzle.models import DailyStatusReport + +# Get today's check-ins +today_reports = DailyStatusReport.objects.filter( + created_at__date=timezone.now().date() +) +``` + +### TimeLog + +Tracks time spent on tasks. + +**Fields**: +- `user` (ForeignKey): User logging time +- `organization` (ForeignKey, nullable): Associated organization (if any) +- `hours` (DecimalField): Hours worked +- `date` (DateField): Date of work +- `task_description` (TextField): What was worked on + +**Example usage**: +``` +from sizzle.models import TimeLog + +# Log time +TimeLog.objects.create( + user=request.user, + hours=3.5, + date=timezone.now().date(), + task_description="Fixed login bug" +) +``` + +### ReminderSettings + +User preferences for reminders. + +**Fields**: +- `user` (OneToOneField): User +- `reminder_time` (TimeField): Preferred reminder time +- `enabled` (BooleanField): Whether reminders are active +- `slack_enabled` (BooleanField): Send via Slack + +## Management Commands + +Sizzle includes several management commands for automated tasks: + +### Daily Check-in Reminders +Send in-app notifications to remind users to submit their daily check-in. + +```bash +python manage.py daily_checkin_reminder +``` + +**What it does:** +- Finds all users in organizations where `check_ins_enabled=True` +- Creates Notification objects for each user +- Links to `/add-sizzle-checkin/` URL + +**Dependencies:** +- UserProfile model (has team field) +- Notification model (website app) +- Organization's `check_ins_enabled` flag + +**Cron setup:** +```bash +0 9 * * * cd /path/to/project && python manage.py daily_checkin_reminder +``` + +### Email Reminder System +Send email reminders to users based on their personal ReminderSettings. + +```bash +python manage.py cron_send_reminders +``` + +**What it does:** +- Checks each user's ReminderSettings for: + - `enabled=True` + - `reminder_time` matches current time + - `reminder_days` includes today +- Checks if user hasn't checked in today (`UserProfile.last_check_in`) +- Sends personalized email reminders +- Logs all activity to `logs/reminder_emails.log` + +**Advanced features:** +- Respects timezone settings +- Configurable days of week +- Tracks reminder history +- Random delays to avoid email spam detection + +**Cron setup (runs every hour):** +```bash +0 * * * * cd /path/to/project && python manage.py cron_send_reminders +``` + +### Slack Daily Timelogs +Post daily timelog summaries to Slack channels. + +```bash +python manage.py slack_daily_timelogs +``` + +**What it does:** +- Runs every hour (checks `current_hour_utc`) +- For each SlackIntegration where: + - `daily_updates=True` + - `daily_update_time` matches current hour +- Fetches all TimeLog entries from last 24 hours for that organization +- Formats summary message with: + - Task names + - Start/end times + - GitHub issue URLs + - Total time worked +- Posts to configured Slack channel + +**Dependencies:** +- SlackIntegration model +- Organization model +- TimeLog model +- slack-bolt Python package +- Valid Slack bot tokens + +**Example output:** +``` +### Time Log Summary ### + +Task: Bug fix - Issue #123 +Start: 2024-11-08 09:00:00 +End: 2024-11-08 11:30:00 +Issue URL: https://github.com/org/repo/issues/123 + +Task: Feature development - Issue #456 +Start: 2024-11-08 13:00:00 +End: 2024-11-08 16:00:00 +Issue URL: https://github.com/org/repo/issues/456 + +Total Time: 5 hours, 30 minutes, 0 seconds +``` + +**Cron setup (runs every hour):** +```bash +0 * * * * cd /path/to/project && python manage.py slack_daily_timelogs +``` + +### Run All Sizzle Daily Tasks +Master command that runs all Sizzle-related daily tasks. + +```bash +python manage.py run_sizzle_daily +``` + +**What it does:** +- Calls `daily_checkin_reminder` +- Calls `cron_send_reminders` +- Provides centralized logging and error handling + +**Cron setup (once daily):** +```bash +0 9 * * * cd /path/to/project && python manage.py run_sizzle_daily +``` + +## URLs Reference + +| URL Pattern | View | Description | +|-------------|------|-------------| +| `/sizzle/` | `sizzle` | Main dashboard | +| `/sizzle/check-in/` | `checkIN` | Check-in list | +| `/sizzle/add-sizzle-checkin/` | `add_sizzle_checkIN` | Submit new check-in | +| `/sizzle/check-in//` | `checkIN_detail` | View specific check-in | +| `/sizzle/time-logs/` | `TimeLogListView` | View time logs | +| `/sizzle/api/timelogsreport/` | `TimeLogListAPIView` | Time log API | +| `/sizzle/sizzle-daily-log/` | `sizzle_daily_log` | Daily log view | +| `/sizzle/user-sizzle-report//` | `user_sizzle_report` | User report | + +## Dependencies + +### Required (Core) + +These are installed automatically with `pip install django-sizzle`: + +- **Django** (>= 4.0): Web framework +- **pytz** (>= 2023.3): Timezone handling + +### Optional Dependencies + +Install with extras for additional features: + +**Slack Integration**: +``` +pip install django-sizzle[slack] +``` +Includes: `slack-bolt >= 1.18.0` + +**Development Tools**: +``` +pip install django-sizzle[dev] +``` +Includes: `pytest`, `pytest-django`, `black`, `flake8` + +## External Dependencies + +Sizzle relies on these Django built-in models (you must have them): + +- **User model**: `django.contrib.auth.models.User` +- **Organization model** (optional): If you have an Organization model, TimeLog can reference it +- **UserProfile model** (optional): If you track `last_check_in` field + +**Note**: Sizzle works standalone, but integrates better if your project has these models. + +## Troubleshooting + +### Configuration Issues + +#### "No module named 'sizzle'" +Make sure you added `'sizzle'` to `INSTALLED_APPS` in `settings.py`. + +#### Model Configuration Validation +Check which models are properly configured: + +```python +# In Django shell or management command +from sizzle.utils.model_loader import validate_model_configuration +import json + +status = validate_model_configuration() +print(json.dumps(status, indent=2)) +``` + +This will show you which models are available: +```json +{ + "slack_integration": true, + "organization": true, + "userprofile": true, + "notification": true, + "reminder_settings": true, + "timelog": true, + "slack_deps": true +} +``` + +#### "SlackIntegration model not configured" +If you see this warning in management commands: +1. Set `SIZZLE_SLACK_ENABLED = False` to disable Slack features +2. Or create a SlackIntegration model and configure `SIZZLE_SLACK_INTEGRATION_MODEL` + +#### "slack-bolt not installed" +Install Slack dependencies: +```bash +pip install slack-bolt +``` + +#### Management Commands Not Working +Test individual commands: +```bash +# Test model loading +python manage.py daily_checkin_reminder + +# Test email system +python manage.py cron_send_reminders + +# Test Slack integration +python manage.py slack_daily_timelogs + +# Run all daily tasks +python manage.py run_sizzle_daily +``` + +### Template Issues + +#### "TemplateDoesNotExist: sizzle/base.html" +Run `python manage.py collectstatic` and ensure `APP_DIRS: True` in `TEMPLATES` settings. + +#### Templates look unstyled +1. Run `python manage.py collectstatic` +2. Make sure `STATIC_URL` is configured in `settings.py` +3. Check browser console for 404 errors on CSS files + +### Database Issues + +#### Migrations not applying +```bash +# Check pending migrations +python manage.py showmigrations sizzle + +# Apply them +python manage.py migrate sizzle +``` + +#### Foreign key errors +If you see foreign key constraint errors, ensure the referenced models exist: +```python +# Check if Organization model exists +SIZZLE_ORGANIZATION_MODEL = 'your_app.Organization' # Make sure this model exists + +# Or set to None if not using organizations +SIZZLE_ORGANIZATION_MODEL = None +``` + +### Feature-Specific Issues + +#### Email Reminders Not Sending +1. Check email settings in Django configuration +2. Verify `SIZZLE_EMAIL_REMINDERS_ENABLED = True` +3. Ensure users have ReminderSettings configured +4. Check logs for SMTP errors + +#### Daily Check-ins Not Working +1. Verify `SIZZLE_DAILY_CHECKINS_ENABLED = True` +2. Check that UserProfile model has `team` field with `check_ins_enabled` +3. Ensure Notification model is properly configured + +#### Slack Integration Issues +1. Verify bot token and channel permissions +2. Check `SIZZLE_SLACK_ENABLED = True` +3. Test Slack connectivity outside Django +4. Review SlackIntegration model configuration + +### Configuration Debugging + +Enable detailed logging for debugging: + +```python +# settings.py +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'sizzle': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': True, + }, + }, +} +``` + +## Development + +### Running Tests + +``` +# Install dev dependencies +pip install django-sizzle[dev] + +# Run tests +pytest +``` + +### Code Style + +We use Black and isort for code formatting: + +``` +black sizzle/ +isort sizzle/ +``` + +## Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/my-feature` +3. Make your changes +4. Run tests: `pytest` +5. Format code: `black sizzle/ && isort sizzle/` +6. Submit a pull request + +## License + +This project is licensed under the AGPL-3.0 License - see LICENSE file for details. + +## Credits + +Developed as part of the OWASP Bug Logging Tool (BLT) project. + +## Support + +- GitHub Issues: https://github.com/OWASP-BLT/BLT/issues +- OWASP BLT: https://owasp.org/www-project-bug-logging-tool/ + +## Changelog + +### Version 0.1.0 (2025-11-08) +- Initial release +- Daily check-in functionality +- Time logging +- Streak tracking +- Email reminders +- Slack integration (optional) +- REST API endpoints + \ No newline at end of file diff --git a/sizzle/__init__.py b/sizzle/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sizzle/admin.py b/sizzle/admin.py new file mode 100644 index 0000000000..98cf44ff3a --- /dev/null +++ b/sizzle/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from .models import DailyStatusReport, ReminderSettings, TimeLog + + +@admin.register(TimeLog) +class TimelogAdmin(admin.ModelAdmin): + pass + + +@admin.register(DailyStatusReport) +class DailyStatusReportAdmin(admin.ModelAdmin): + pass + + +@admin.register(ReminderSettings) +class ReminderSettingsAdmin(admin.ModelAdmin): + pass diff --git a/sizzle/apps.py b/sizzle/apps.py new file mode 100644 index 0000000000..7650cef23f --- /dev/null +++ b/sizzle/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SizzleConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "sizzle" diff --git a/sizzle/conf.py b/sizzle/conf.py new file mode 100644 index 0000000000..9b993c234b --- /dev/null +++ b/sizzle/conf.py @@ -0,0 +1,71 @@ +""" +Configuration for Sizzle app with sensible defaults for BLT. +Follows Django's AUTH_USER_MODEL pattern for swappable models. +""" +from django.conf import settings + +# Sizzle configuration - makes the plugin flexible for different Django projects +SIZZLE_SETTINGS = { + # Default base template for standalone projects + "BASE_TEMPLATE": getattr(settings, "SIZZLE_BASE_TEMPLATE", "base.html"), + # Parent base template - set this in your main project for integration + "PARENT_BASE": getattr(settings, "SIZZLE_PARENT_BASE", None), + # Whether to integrate with the parent project's layout + "USE_PROJECT_BASE": getattr(settings, "SIZZLE_USE_PROJECT_BASE", True), + # Show sidenav if available (for BLT integration) + "SHOW_SIDENAV": getattr(settings, "SIZZLE_SHOW_SIDENAV", True), +} + +# =============================== +# Model Configuration (Swappable) +# =============================== + +# Slack Integration Model (swappable) +SIZZLE_SLACK_INTEGRATION_MODEL = getattr( + settings, + "SIZZLE_SLACK_INTEGRATION_MODEL", + "website.SlackIntegration", # Default for BLT +) + +# Organization Model (swappable) +SIZZLE_ORGANIZATION_MODEL = getattr( + settings, + "SIZZLE_ORGANIZATION_MODEL", + "website.Organization", # Default for BLT +) + +# UserProfile Model (swappable) +SIZZLE_USERPROFILE_MODEL = getattr( + settings, + "SIZZLE_USERPROFILE_MODEL", + "website.UserProfile", # Default for BLT +) + +# Notification Model (swappable) +SIZZLE_NOTIFICATION_MODEL = getattr( + settings, + "SIZZLE_NOTIFICATION_MODEL", + "website.Notification", # Default for BLT +) + +# =============================== +# Feature Flags +# =============================== + +# Enable/disable features +SIZZLE_SLACK_ENABLED = getattr( + settings, + "SIZZLE_SLACK_ENABLED", + True, # Enabled by default for BLT +) + +SIZZLE_EMAIL_REMINDERS_ENABLED = getattr(settings, "SIZZLE_EMAIL_REMINDERS_ENABLED", True) + +SIZZLE_DAILY_CHECKINS_ENABLED = getattr(settings, "SIZZLE_DAILY_CHECKINS_ENABLED", True) + + +def get_base_template(): + """Get the appropriate base template for sizzle templates""" + if SIZZLE_SETTINGS["PARENT_BASE"]: + return SIZZLE_SETTINGS["PARENT_BASE"] + return SIZZLE_SETTINGS["BASE_TEMPLATE"] diff --git a/sizzle/context_processors.py b/sizzle/context_processors.py new file mode 100644 index 0000000000..1fcfb826cc --- /dev/null +++ b/sizzle/context_processors.py @@ -0,0 +1,29 @@ +# sizzle/context_processors.py +from django.conf import settings +from django.template import TemplateDoesNotExist +from django.template.loader import get_template + + +def sizzle_context(request): + """Provide sizzle-specific template context for seamless integration""" + + # Check if sidenav should be shown based on setting + show_sidenav = getattr(settings, "SIZZLE_SHOW_SIDENAV", True) + + # Only check if the template exists if the setting allows it + if show_sidenav: + try: + get_template("includes/sidenav.html") + has_sidenav = True + except TemplateDoesNotExist: + has_sidenav = False + else: + has_sidenav = False + + # Get the base template to extend from project settings or use default + parent_base = getattr(settings, "SIZZLE_PARENT_BASE", None) + + return { + "sizzle_has_sidenav": has_sidenav, + "parent_base": parent_base, # This allows templates to extend the right base + } diff --git a/sizzle/management/__init__.py b/sizzle/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sizzle/management/base.py b/sizzle/management/base.py new file mode 100644 index 0000000000..f69f245fbd --- /dev/null +++ b/sizzle/management/base.py @@ -0,0 +1,26 @@ +import logging + +from django.core.management.base import BaseCommand + + +class SizzleBaseCommand(BaseCommand): + """Base command class for Sizzle management commands""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = logging.getLogger(self.__class__.__module__) + + def log_info(self, message): + """Log info message""" + self.stdout.write(self.style.SUCCESS(message)) + self.logger.info(message) + + def log_error(self, message): + """Log error message""" + self.stdout.write(self.style.ERROR(message)) + self.logger.error(message) + + def log_warning(self, message): + """Log warning message""" + self.stdout.write(self.style.WARNING(message)) + self.logger.warning(message) diff --git a/sizzle/management/commands/__init__.py b/sizzle/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sizzle/management/commands/cron_send_reminders.py b/sizzle/management/commands/cron_send_reminders.py new file mode 100644 index 0000000000..05f1379b57 --- /dev/null +++ b/sizzle/management/commands/cron_send_reminders.py @@ -0,0 +1,275 @@ +import logging +import os +import random +import time +from datetime import time as dt_time + +from django.conf import settings +from django.core.mail import EmailMultiAlternatives +from django.db.models import Q +from django.utils import timezone + +from sizzle.conf import SIZZLE_EMAIL_REMINDERS_ENABLED +from sizzle.management.base import SizzleBaseCommand +from sizzle.utils.model_loader import get_reminder_settings_model, get_userprofile_model + +logger = logging.getLogger(__name__) + + +class Command(SizzleBaseCommand): + help = "Sends daily check-in reminders to users who haven't checked in today" + + def setup_logging(self): + logs_dir = os.path.join(settings.BASE_DIR, "logs") + os.makedirs(logs_dir, exist_ok=True) + log_file = os.path.join(logs_dir, "reminder_emails.log") + handler = logging.FileHandler(log_file) + handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + return handler + + def handle(self, *args, **options): + # Check if email reminders are enabled + if not SIZZLE_EMAIL_REMINDERS_ENABLED: + self.log_warning("Email reminders are disabled in settings") + return + + # Get models dynamically + ReminderSettings = get_reminder_settings_model() + if ReminderSettings is None: + self.log_error("ReminderSettings model not available. Ensure sizzle migrations are run.") + return + + UserProfile = get_userprofile_model() + if UserProfile is None: + self.log_warning( + "UserProfile model not configured. Check-in status verification will be skipped. " + "Check SIZZLE_USERPROFILE_MODEL setting." + ) + + handler = self.setup_logging() + try: + now = timezone.now() + logger.info(f"Starting reminder process at {now} (UTC)") + + # Calculate the current 15-minute window in UTC + current_hour = now.hour + current_minute = now.minute + window_start_minute = (current_minute // 15) * 15 + window_end_minute = window_start_minute + 15 + window_start_hour = current_hour + window_end_hour = current_hour + if window_end_minute >= 60: + window_end_minute -= 60 + window_end_hour = (current_hour + 1) % 24 + + window_start_time = dt_time(hour=window_start_hour, minute=window_start_minute) + window_end_time = dt_time(hour=window_end_hour, minute=window_end_minute) + wraps_midnight = window_end_hour < window_start_hour or ( + window_end_hour == window_start_hour and window_end_minute < window_start_minute + ) + if wraps_midnight: + time_window_filter = Q(reminder_time_utc__gte=window_start_time) | Q( + reminder_time_utc__lt=window_end_time + ) + else: + time_window_filter = Q( + reminder_time_utc__gte=window_start_time, + reminder_time_utc__lt=window_end_time, + ) + + logger.info(f"Current UTC time: {now.strftime('%Y-%m-%d %H:%M:%S')}") + logger.info( + f"Processing reminders for UTC time window: {window_start_time.strftime('%H:%M')} - {window_end_time.strftime('%H:%M')}" + ) + logger.info(f"Time window in minutes: {window_start_minute} - {window_end_minute}") + + # Get active reminder settings within the current UTC time window + # Exclude users who already received a reminder today + active_settings = ( + ReminderSettings.objects.filter(is_active=True) + .filter(time_window_filter) + .exclude(last_reminder_sent__date=now.date()) + ) + + logger.info(f"Found {active_settings.count()} users with reminders in current UTC time window") + + # Prefetch user profiles if UserProfile model is available + user_ids = [rs.user_id for rs in active_settings] + profile_map = {} + if UserProfile: + try: + profile_map = { + profile.user_id: profile for profile in UserProfile.objects.filter(user_id__in=user_ids) + } + except Exception as e: + logger.warning(f"Could not fetch user profiles: {e}") + + reminders_to_send = [] + + for reminder_settings in active_settings: + try: + # Check if user has checked in today using prefetched profiles (if available) + if UserProfile and profile_map: + profile = profile_map.get(reminder_settings.user_id) + if ( + profile + and hasattr(profile, "last_check_in") + and profile.last_check_in + and profile.last_check_in == now.date() + ): + continue + + reminders_to_send.append( + (reminder_settings, profile_map.get(reminder_settings.user_id) if profile_map else None) + ) + logger.info( + f"User {reminder_settings.user.username} added to reminder list for time {reminder_settings.reminder_time} ({reminder_settings.timezone})" + ) + + except Exception as e: + logger.error(f"Error processing user {reminder_settings.user.username}: {str(e)}") + continue + + if not reminders_to_send: + logger.info("No users need reminders at this time") + return + + # Process reminders individually for personalization + successful_count = 0 + failed_count = 0 + total_users = len(reminders_to_send) + + for i, (reminder_settings, profile) in enumerate(reminders_to_send, 1): + try: + # Add small delay between emails to avoid overwhelming the server + if i > 1: + # Small delay between each email + delay = random.uniform(0.1, 0.3) + time.sleep(delay) + # Larger delay every 10 emails + if i % 10 == 0: + extra_delay = random.uniform(1, 2) + logger.info(f"Processed {i} emails, waiting {extra_delay:.2f} seconds") + time.sleep(extra_delay) + + user = reminder_settings.user + domain = getattr( + settings, + "SIZZLE_REMINDER_DOMAIN", + getattr(settings, "PRODUCTION_DOMAIN", None), + ) + if not domain: + logger.error( + "Skipping reminder for %s: no domain configured (set SIZZLE_REMINDER_DOMAIN or PRODUCTION_DOMAIN).", + user.username, + ) + failed_count += 1 + continue + checkin_url = f"https://{domain}/add-sizzle-checkin/" + settings_url = f"https://{domain}/reminder-settings/" + + # Get organization info + org_name = "" + org_info_html = "" + if profile and hasattr(profile, "team") and profile.team: + org_name = profile.team.name + org_info_html = f""" +
+

Organization: {org_name}

+
+ """ + + # Format reminder time in user's timezone + reminder_time_str = reminder_settings.reminder_time.strftime("%I:%M %p") + timezone_str = reminder_settings.timezone + + # Create email message + plain_body = f"""Hello {user.username}, + +This is your daily check-in reminder{f" for {org_name}" if org_name else ""}. + +Reminder Time: {reminder_time_str} ({timezone_str}) + +Click here to check in: {checkin_url} + +You can manage your reminder settings at: {settings_url} + +Regular check-ins help keep your team informed about your progress and any challenges you might be facing. + +Thank you for keeping your team updated! + +Best regards, +The BLT Team""" + + email = EmailMultiAlternatives( + subject="Daily Check-in Reminder", + body=plain_body, + from_email=settings.DEFAULT_FROM_EMAIL, + to=[user.email], + ) + + # Add HTML content + html_content = f""" + + +
+

Daily Check-in Reminder

+

Hello {user.username},

+

It's time for your daily check-in{f" for {org_name}" if org_name else ""}! Please log in to update your status.

+ {org_info_html} +
+

Your Reminder Time: {reminder_time_str} ({timezone_str})

+
+ +

Regular check-ins help keep your team informed about your progress and any challenges you might be facing.

+ +

Thank you for keeping your team updated!

+

Best regards,
The BLT Team

+
+ + + """ + email.attach_alternative(html_content, "text/html") + + # Send email + email.send() + + # Update last_reminder_sent + reminder_settings.last_reminder_sent = now + reminder_settings.save(update_fields=["last_reminder_sent"]) + + successful_count += 1 + logger.info(f"Successfully sent reminder to {user.username} ({user.email})") + + except Exception as e: + failed_count += 1 + logger.error(f"Error sending reminder to {reminder_settings.user.username}: {str(e)}") + + # Log summary + logger.info( + f""" + Reminder Summary: + - Total users processed: {total_users} + - Successfully sent: {successful_count} + - Failed: {failed_count} + """ + ) + + return f"Processed {total_users} users, {successful_count} sent successfully, {failed_count} failed" + except Exception as e: + logger.error(f"Critical error in reminder process: {str(e)}") + raise + finally: + logger.removeHandler(handler) + handler.close() diff --git a/sizzle/management/commands/daily_checkin_reminder.py b/sizzle/management/commands/daily_checkin_reminder.py new file mode 100644 index 0000000000..a03c86b271 --- /dev/null +++ b/sizzle/management/commands/daily_checkin_reminder.py @@ -0,0 +1,64 @@ +from sizzle.conf import SIZZLE_DAILY_CHECKINS_ENABLED +from sizzle.management.base import SizzleBaseCommand +from sizzle.utils.model_loader import get_notification_model, get_userprofile_model + + +class Command(SizzleBaseCommand): + help = "Sends daily check-in reminders to users in organizations with check-ins enabled" + + def handle(self, *args, **options): + # Check if daily check-ins are enabled + if not SIZZLE_DAILY_CHECKINS_ENABLED: + self.log_warning("Daily check-ins are disabled in settings") + return + + # Get models dynamically + UserProfile = get_userprofile_model() + if UserProfile is None: + self.log_error("UserProfile model not configured or available. " "Check SIZZLE_USERPROFILE_MODEL setting.") + return + + Notification = get_notification_model() + if Notification is None: + self.log_error( + "Notification model not configured or available. " "Check SIZZLE_NOTIFICATION_MODEL setting." + ) + return + + try: + # Check if UserProfile has the required fields + userprofiles_with_checkins = UserProfile.objects.filter(team__check_ins_enabled=True) + + notifications = [] + for userprofile in userprofiles_with_checkins: + try: + notifications.append( + Notification( + user=userprofile.user, + message=f"This is a reminder to add your daily check-in for {userprofile.team.name}", + notification_type="reminder", + link="/add-sizzle-checkin/", + ) + ) + except Exception as e: + self.log_error(f"Error creating notification for user {userprofile.user.username}: {e}") + continue + + if notifications: + try: + Notification.objects.bulk_create(notifications) + self.log_info(f"Sent check-in reminder notifications to {len(notifications)} users.") + except Exception as e: + self.log_error(f"Error bulk creating notifications: {e}") + else: + self.log_info("No users found with check-ins enabled or no notifications to create.") + + except Exception as e: + self.log_error(f"Error in daily check-in reminder process: {e}") + # Check if it's a field-related error and provide helpful guidance + if "team" in str(e) or "check_ins_enabled" in str(e): + self.log_error( + "It appears your UserProfile model does not have the expected fields. " + 'Sizzle expects UserProfile to have a "team" field with "check_ins_enabled" attribute.' + ) + raise diff --git a/sizzle/management/commands/run_sizzle_daily.py b/sizzle/management/commands/run_sizzle_daily.py new file mode 100644 index 0000000000..034e4da57e --- /dev/null +++ b/sizzle/management/commands/run_sizzle_daily.py @@ -0,0 +1,56 @@ +import logging + +from django.core.management import call_command +from django.utils import timezone + +from sizzle.conf import SIZZLE_DAILY_CHECKINS_ENABLED, SIZZLE_EMAIL_REMINDERS_ENABLED, SIZZLE_SLACK_ENABLED +from sizzle.management.base import SizzleBaseCommand + +logger = logging.getLogger(__name__) + + +class Command(SizzleBaseCommand): + help = "Runs all Sizzle-related commands scheduled to execute daily" + + def handle(self, *args, **options): + try: + self.log_info(f"Starting daily Sizzle tasks at {timezone.now()}") + + # Run daily check-in reminders if enabled + if SIZZLE_DAILY_CHECKINS_ENABLED: + try: + self.log_info("Running daily check-in reminders...") + call_command("daily_checkin_reminder") + self.log_info("Daily check-in reminders completed successfully") + except Exception as e: + self.log_error(f"Error sending daily checkin reminders: {str(e)}") + else: + self.log_info("Daily check-in reminders are disabled in settings") + + # Run email reminders based on user settings if enabled + if SIZZLE_EMAIL_REMINDERS_ENABLED: + try: + self.log_info("Running email reminder system...") + call_command("cron_send_reminders") + self.log_info("Email reminder system completed successfully") + except Exception as e: + self.log_error(f"Error sending user reminders: {str(e)}") + else: + self.log_info("Email reminders are disabled in settings") + + # Run Slack notifications if enabled + if SIZZLE_SLACK_ENABLED: + try: + self.log_info("Running Slack daily timelogs...") + call_command("slack_daily_timelogs") + self.log_info("Slack daily timelogs completed successfully") + except Exception as e: + self.log_error(f"Error sending Slack timelogs: {str(e)}") + else: + self.log_info("Slack integration is disabled in settings") + + self.log_info("All daily Sizzle tasks completed") + + except Exception as e: + self.log_error(f"Critical error in daily Sizzle tasks: {str(e)}") + raise diff --git a/sizzle/management/commands/slack_daily_timelogs.py b/sizzle/management/commands/slack_daily_timelogs.py new file mode 100644 index 0000000000..f14b53e644 --- /dev/null +++ b/sizzle/management/commands/slack_daily_timelogs.py @@ -0,0 +1,122 @@ +from datetime import timedelta + +from django.utils import timezone + +from sizzle.conf import SIZZLE_SLACK_ENABLED +from sizzle.management.base import SizzleBaseCommand +from sizzle.utils.model_loader import check_slack_dependencies, get_slack_integration_model, get_timelog_model + + +class Command(SizzleBaseCommand): + help = "Sends messages to organizations with a Slack integration for Sizzle timelogs to be run every hour." + + def handle(self, *args, **kwargs): + # Check if Slack is enabled in settings + if not SIZZLE_SLACK_ENABLED: + self.log_warning("Slack integration is disabled in settings") + return + + # Check if slack-bolt is available + slack_available, slack_error = check_slack_dependencies() + if not slack_available: + self.log_error(f"Slack dependencies not available: {slack_error}") + return + + # Get models dynamically + SlackIntegration = get_slack_integration_model() + if SlackIntegration is None: + self.log_error( + "SlackIntegration model not configured or available. " "Check SIZZLE_SLACK_INTEGRATION_MODEL setting." + ) + return + + TimeLog = get_timelog_model() + if TimeLog is None: + self.log_error("TimeLog model not available. Ensure sizzle migrations are run.") + return + + # Import Slack dependencies after validation + + # Get the current time and UTC hour + now = timezone.now() + current_hour_utc = now.astimezone(timezone.utc).hour + + # Fetch all Slack integrations with related integration data + try: + slack_integrations = SlackIntegration.objects.select_related("integration__organization").all() + except Exception as e: + self.log_error(f"Error fetching Slack integrations: {e}") + return + + processed_count = 0 + for integration in slack_integrations: + try: + current_org = integration.integration.organization + if ( + integration.default_channel_id + and current_org + and integration.daily_updates + # Ensure it's the correct hour + and integration.daily_update_time == current_hour_utc + ): + self.log_info(f"Processing updates for organization: {current_org.name}") + + last_24_hours = now - timedelta(hours=24) + + timelog_history = TimeLog.objects.filter( + organization=current_org, + start_time__isnull=False, + end_time__isnull=False, + end_time__gte=last_24_hours, # Ended in the last 24 hours + ) + + if timelog_history.exists(): + total_time = timedelta() + summary_message = "### Time Log Summary ###\n\n" + + for timelog in timelog_history: + st = timelog.start_time + et = timelog.end_time + issue_url = timelog.github_issue_url if timelog.github_issue_url else "No issue URL" + summary_message += ( + f"Task: {timelog}\n" f"Start: {st}\n" f"End: {et}\n" f"Issue URL: {issue_url}\n\n" + ) + total_time += et - st + + human_friendly_total_time = self.format_timedelta(total_time) + summary_message += f"Total Time: {human_friendly_total_time}" + + self.send_message( + integration.default_channel_id, + integration.bot_access_token, + summary_message, + ) + processed_count += 1 + else: + self.log_info(f"No timelogs found for organization: {current_org.name}") + + except Exception as e: + self.log_error(f"Error processing integration for organization: {e}") + continue + + self.log_info(f"Processed {processed_count} Slack integrations successfully") + + def format_timedelta(self, td): + """Convert a timedelta object into a human-readable string.""" + total_seconds = int(td.total_seconds()) + hours, remainder = divmod(total_seconds, 3600) + minutes, seconds = divmod(remainder, 60) + return f"{hours} hours, {minutes} minutes, {seconds} seconds" + + def send_message(self, channel_id, bot_token, message): + """Send a message to the Slack channel.""" + try: + # Import here after dependency validation + from slack_bolt import App + + app = App(token=bot_token) + app.client.conversations_join(channel=channel_id) + response = app.client.chat_postMessage(channel=channel_id, text=message) + self.log_info(f"Message sent successfully: {response['ts']}") + except Exception as e: + self.log_error(f"Error sending message: {e}") diff --git a/sizzle/migrations/0001_initial.py b/sizzle/migrations/0001_initial.py new file mode 100644 index 0000000000..a5f6799823 --- /dev/null +++ b/sizzle/migrations/0001_initial.py @@ -0,0 +1,111 @@ +# Generated by Django 4.2.24 on 2025-11-08 07:01 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.db import migrations, models + +from sizzle.conf import SIZZLE_ORGANIZATION_MODEL + +if not SIZZLE_ORGANIZATION_MODEL: + raise ImproperlyConfigured("SIZZLE_ORGANIZATION_MODEL must be configured before running sizzle migrations.") + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("website", "0246_add_user_progress_models"), + ] + + operations = [ + migrations.CreateModel( + name="TimeLog", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("start_time", models.DateTimeField()), + ("end_time", models.DateTimeField(blank=True, null=True)), + ("duration", models.DurationField(blank=True, null=True)), + ("github_issue_url", models.URLField(blank=True, null=True)), + ("created", models.DateTimeField(default=django.utils.timezone.now, editable=False)), + ( + "organization", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="sizzle_time_logs", + to=settings.SIZZLE_ORGANIZATION_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sizzle_time_logs", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="ReminderSettings", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("reminder_time", models.TimeField(help_text="Time to send daily reminders (in user's timezone)")), + ( + "reminder_time_utc", + models.TimeField(blank=True, help_text="Time to send daily reminders (in UTC)", null=True), + ), + ("timezone", models.CharField(default="UTC", max_length=50)), + ("is_active", models.BooleanField(default=True, help_text="Enable/disable daily reminders")), + ("last_reminder_sent", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="sizzle_reminder_settings", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Reminder Settings", + "verbose_name_plural": "Reminder Settings", + "indexes": [ + models.Index(fields=["is_active"], name="sizzle_remi_is_acti_dde965_idx"), + models.Index(fields=["reminder_time_utc"], name="sizzle_remi_reminde_8c6dc9_idx"), + ], + }, + ), + migrations.CreateModel( + name="DailyStatusReport", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("date", models.DateField()), + ("previous_work", models.TextField()), + ("next_plan", models.TextField()), + ("blockers", models.TextField()), + ("goal_accomplished", models.BooleanField(default=False)), + ("current_mood", models.CharField(default="Happy 😊", max_length=50)), + ("created", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sizzle_daily_status_reports", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-date"], + "indexes": [models.Index(fields=["user", "date"], name="sizzle_dail_user_id_667d52_idx")], + "unique_together": {("user", "date")}, + }, + ), + ] diff --git a/sizzle/migrations/__init__.py b/sizzle/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sizzle/models.py b/sizzle/models.py new file mode 100644 index 0000000000..de110ff56e --- /dev/null +++ b/sizzle/models.py @@ -0,0 +1,104 @@ +import logging +from datetime import datetime + +import pytz +from django.conf import settings +from django.db import models +from django.utils import timezone + +logger = logging.getLogger(__name__) + + +class TimeLog(models.Model): + """Time tracking model for sizzle functionality""" + + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="sizzle_time_logs") + organization = models.ForeignKey( + getattr(settings, "SIZZLE_ORGANIZATION_MODEL", "website.Organization"), + on_delete=models.CASCADE, + related_name="sizzle_time_logs", + null=True, + blank=True, + ) + start_time = models.DateTimeField() + end_time = models.DateTimeField(null=True, blank=True) + duration = models.DurationField(null=True, blank=True) + github_issue_url = models.URLField(null=True, blank=True) + created = models.DateTimeField(default=timezone.now, editable=False) + + def save(self, *args, **kwargs): + if self.end_time and self.start_time <= self.end_time: + self.duration = self.end_time - self.start_time + super().save(*args, **kwargs) + + def __str__(self): + return f"TimeLog by {self.user.username} from {self.start_time} to {self.end_time}" + + +class DailyStatusReport(models.Model): + """Daily status report for team check-ins""" + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="sizzle_daily_status_reports" + ) + date = models.DateField() + previous_work = models.TextField() + next_plan = models.TextField() + blockers = models.TextField() + goal_accomplished = models.BooleanField(default=False) + current_mood = models.CharField(max_length=50, default="Happy 😊") + created = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ("user", "date") + ordering = ["-date"] + indexes = [ + models.Index(fields=["user", "date"]), + ] + + def __str__(self): + return f"Daily Status Report by {self.user.username} on {self.date}" + + +class ReminderSettings(models.Model): + """User settings for daily reminder notifications""" + + user = models.OneToOneField( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="sizzle_reminder_settings" + ) + reminder_time = models.TimeField(help_text="Time to send daily reminders (in user's timezone)") + reminder_time_utc = models.TimeField(help_text="Time to send daily reminders (in UTC)", null=True, blank=True) + timezone = models.CharField(max_length=50, default="UTC") + is_active = models.BooleanField(default=True, help_text="Enable/disable daily reminders") + last_reminder_sent = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "Reminder Settings" + verbose_name_plural = "Reminder Settings" + indexes = [ + models.Index(fields=["is_active"]), + models.Index(fields=["reminder_time_utc"]), + ] + + def __str__(self): + return f"Reminder Settings for {self.user.username}" + + def save(self, *args, **kwargs): + if self.reminder_time and self.timezone: + user_tz = pytz.timezone(self.timezone) + # Create a datetime with today's date and the reminder time + today = timezone.now().date() + local_dt = user_tz.localize(datetime.combine(today, self.reminder_time)) + # Convert to UTC + utc_dt = local_dt.astimezone(pytz.UTC) + # Extract just the time part + self.reminder_time_utc = utc_dt.time() + super().save(*args, **kwargs) + + @classmethod + def get_timezone_choices(cls): + if not hasattr(cls, "_timezone_choices"): + cls._timezone_choices = [(tz, tz) for tz in pytz.common_timezones] + return cls._timezone_choices diff --git a/website/static/img/sizzle/app_finding_1.jpg b/sizzle/static/images/app_finding_1.jpg similarity index 100% rename from website/static/img/sizzle/app_finding_1.jpg rename to sizzle/static/images/app_finding_1.jpg diff --git a/website/static/img/sizzle/app_finding_2.jpg b/sizzle/static/images/app_finding_2.jpg similarity index 100% rename from website/static/img/sizzle/app_finding_2.jpg rename to sizzle/static/images/app_finding_2.jpg diff --git a/website/static/img/sizzle/running_sizzle_1.jpg b/sizzle/static/images/running_sizzle_1.jpg similarity index 100% rename from website/static/img/sizzle/running_sizzle_1.jpg rename to sizzle/static/images/running_sizzle_1.jpg diff --git a/website/static/img/sizzle/running_sizzle_2.jpg b/sizzle/static/images/running_sizzle_2.jpg similarity index 100% rename from website/static/img/sizzle/running_sizzle_2.jpg rename to sizzle/static/images/running_sizzle_2.jpg diff --git a/website/static/img/sizzle/running_sizzle_3.jpg b/sizzle/static/images/running_sizzle_3.jpg similarity index 100% rename from website/static/img/sizzle/running_sizzle_3.jpg rename to sizzle/static/images/running_sizzle_3.jpg diff --git a/website/static/img/sizzle/running_sizzle_4.jpg b/sizzle/static/images/running_sizzle_4.jpg similarity index 100% rename from website/static/img/sizzle/running_sizzle_4.jpg rename to sizzle/static/images/running_sizzle_4.jpg diff --git a/website/static/img/sizzle/running_sizzle_5.jpg b/sizzle/static/images/running_sizzle_5.jpg similarity index 100% rename from website/static/img/sizzle/running_sizzle_5.jpg rename to sizzle/static/images/running_sizzle_5.jpg diff --git a/website/static/img/sizzle/running_sizzle_6.jpg b/sizzle/static/images/running_sizzle_6.jpg similarity index 100% rename from website/static/img/sizzle/running_sizzle_6.jpg rename to sizzle/static/images/running_sizzle_6.jpg diff --git a/website/templates/sizzle/add_sizzle_checkin.html b/sizzle/templates/add_sizzle_checkin.html similarity index 98% rename from website/templates/sizzle/add_sizzle_checkin.html rename to sizzle/templates/add_sizzle_checkin.html index 58bb36aaf1..5728306dce 100644 --- a/website/templates/sizzle/add_sizzle_checkin.html +++ b/sizzle/templates/add_sizzle_checkin.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends parent_base|default:"base.html" %} {% load static %} {% block content %}
- {% include "includes/sidenav.html" %} + {% if sizzle_has_sidenav %} + {% include "includes/sidenav.html" %} + {% endif %}

Add Daily Check-In

diff --git a/sizzle/templates/base.html b/sizzle/templates/base.html new file mode 100644 index 0000000000..2b476e04a8 --- /dev/null +++ b/sizzle/templates/base.html @@ -0,0 +1,35 @@ +{% load static %} + + + + + + + + + {% block title %}Sizzle{% endblock %} + + {# Allow project to inject their own CSS #} + {% block extra_head %}{% endblock %} + {# Minimal default styling or use Tailwind CDN #} + + + + {# Optional navigation - can be overridden #} + {% block navigation %} + + {% endblock %} + {# Main content area #} +
+ {% block content %}{% endblock %} +
+ {# Optional footer #} + {% block footer %}{% endblock %} + {# Allow project to inject scripts #} + {% block extra_scripts %}{% endblock %} + + diff --git a/website/templates/sizzle/checkin.html b/sizzle/templates/checkin.html similarity index 98% rename from website/templates/sizzle/checkin.html rename to sizzle/templates/checkin.html index 2af0731e94..dbed6826b9 100644 --- a/website/templates/sizzle/checkin.html +++ b/sizzle/templates/checkin.html @@ -1,11 +1,13 @@ -{% extends "base.html" %} +{% extends parent_base|default:"base.html" %} {% load static %} {% block content %}
- {% include "includes/sidenav.html" %} + {% if sizzle_has_sidenav %} + {% include "includes/sidenav.html" %} + {% endif %}

Check-In Reports

diff --git a/website/templates/sizzle/checkin_detail.html b/sizzle/templates/checkin_detail.html similarity index 92% rename from website/templates/sizzle/checkin_detail.html rename to sizzle/templates/checkin_detail.html index b17cf42914..a239ee6373 100644 --- a/website/templates/sizzle/checkin_detail.html +++ b/sizzle/templates/checkin_detail.html @@ -1,8 +1,10 @@ -{% extends "base.html" %} +{% extends parent_base|default:"base.html" %} {% load static %} {% block content %}
- {% include "includes/sidenav.html" %} + {% if sizzle_has_sidenav %} + {% include "includes/sidenav.html" %} + {% endif %}

Check-In Details

diff --git a/website/templates/sizzle/sizzle.html b/sizzle/templates/sizzle.html similarity index 98% rename from website/templates/sizzle/sizzle.html rename to sizzle/templates/sizzle.html index 436dad6287..fee41d96d5 100644 --- a/website/templates/sizzle/sizzle.html +++ b/sizzle/templates/sizzle.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends parent_base|default:"base.html" %} {% load static %} {% block title %} Sizzle @@ -21,7 +21,9 @@ href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
- {% include "includes/sidenav.html" %} + {% if sizzle_has_sidenav %} + {% include "includes/sidenav.html" %} + {% endif %}
@@ -37,7 +39,7 @@

Your Sizzle Report

{% endif %}
- Sizzle Docs diff --git a/website/templates/sizzle/sizzle_daily_status.html b/sizzle/templates/sizzle_daily_status.html similarity index 95% rename from website/templates/sizzle/sizzle_daily_status.html rename to sizzle/templates/sizzle_daily_status.html index 41a09ebe81..47bd9584db 100644 --- a/website/templates/sizzle/sizzle_daily_status.html +++ b/sizzle/templates/sizzle_daily_status.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends parent_base|default:"base.html" %} {% load static %} {% block title %} Sizzle Daily Status Report @@ -18,7 +18,9 @@ {% block content %}
- {% include "includes/sidenav.html" %} + {% if sizzle_has_sidenav %} + {% include "includes/sidenav.html" %} + {% endif %}
@@ -29,7 +31,7 @@

Your Sizzle Daily Status R class="bg-red-500 text-white py-2 px-6 rounded-md shadow-md hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-300 cursor-pointer"> Back to Sizzle - Sizzle Docs diff --git a/website/templates/sizzle/sizzle_docs.html b/sizzle/templates/sizzle_docs.html similarity index 93% rename from website/templates/sizzle/sizzle_docs.html rename to sizzle/templates/sizzle_docs.html index 613d36d3b0..7b4b317dac 100644 --- a/website/templates/sizzle/sizzle_docs.html +++ b/sizzle/templates/sizzle_docs.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends parent_base|default:"base.html" %} {% load static %} {% block title %} How to Use Sizzle @@ -18,7 +18,9 @@ {% block content %}
- {% include "includes/sidenav.html" %} + {% if sizzle_has_sidenav %} + {% include "includes/sidenav.html" %} + {% endif %}

How to Use Sizzle

@@ -45,7 +47,7 @@

How to Use Sizzle<
Find the Latest Successful Build

Locate the most recent successful build and click on it to proceed with the download.

- Finding the App on GitHub ActionsHow to Use Sizzle<
Download the App for Your Device

Select the appropriate version of the app for your device and download it.

- Choosing Device Version to DownloadHow to Use Sizzle<
Open the App and Login

Once downloaded, open the app and login using your credentials.

- Logging into the AppHow to Use Sizzle<
Access the Side Menu and Click "Sizzle"

Open the side menu and tap on the "Sizzle" button to continue.

- Accessing Side MenuHow to Use Sizzle<
Enter Your GitHub Username

Enter your GitHub username in the provided field and click "Add Username".

- Entering GitHub UsernameHow to Use Sizzle<
Start Time Tracking

Click on the "Start Work" button to begin tracking your work time.

- Starting Time TrackingHow to Use Sizzle< All assigned issues will be displayed here. Select any task to begin working and click on the "Run" button to start tracking time for that task.

- Viewing and Selecting TasksHow to Use Sizzle< Congratulations! You are now tracking time for the selected task. Click on the "Stop" button to stop tracking once completed.

- Tracking Time for Selected Task -{% extends "base.html" %} +{% extends parent_base|default:"base.html" %} {% load custom_filters %} {% load static %} {% block title %} @@ -21,7 +21,9 @@
- {% include "includes/sidenav.html" %} + {% if sizzle_has_sidenav %} + {% include "includes/sidenav.html" %} + {% endif %}
diff --git a/website/templates/sizzle/user_sizzle_report.html b/sizzle/templates/user_sizzle_report.html similarity index 83% rename from website/templates/sizzle/user_sizzle_report.html rename to sizzle/templates/user_sizzle_report.html index 4383663177..85f2716967 100644 --- a/website/templates/sizzle/user_sizzle_report.html +++ b/sizzle/templates/user_sizzle_report.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends parent_base|default:"base.html" %} {% load static %} {% block title %} {{ user.username }}'s Sizzle Report @@ -16,7 +16,9 @@ Access the Sizzle report for {{ user.username }} to track daily activities, issue titles, durations, and start times. Get an overview of their work progress and time management. {% endblock og_description %} {% block content %} - {% include "includes/sidenav.html" %} + {% if sizzle_has_sidenav %} + {% include "includes/sidenav.html" %} + {% endif %}

{{ user.username }}'s Sizzle Report

@@ -44,11 +46,15 @@

{{ user.username }}'s Sizzle Report

{% endblock content %} diff --git a/sizzle/tests.py b/sizzle/tests.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sizzle/urls.py b/sizzle/urls.py new file mode 100644 index 0000000000..791731f61e --- /dev/null +++ b/sizzle/urls.py @@ -0,0 +1,29 @@ +from django.urls import path + +from sizzle.views import ( + TimeLogListAPIView, + TimeLogListView, + add_sizzle_checkIN, + checkIN, + checkIN_detail, + sizzle, + sizzle_daily_log, + sizzle_docs, + user_sizzle_report, +) + +urlpatterns = [ + path("", sizzle, name="sizzle"), + path("check-in/", checkIN, name="checkIN"), + path("add-sizzle-checkin/", add_sizzle_checkIN, name="add_sizzle_checkin"), + path("check-in//", checkIN_detail, name="checkIN_detail"), + path("docs/", sizzle_docs, name="sizzle_docs"), + path("api/timelogsreport/", TimeLogListAPIView, name="timelogsreport"), + path("time-logs/", TimeLogListView, name="time_logs"), + path("sizzle-daily-log/", sizzle_daily_log, name="sizzle_daily_log"), + path( + "user-sizzle-report//", + user_sizzle_report, + name="user_sizzle_report", + ), +] diff --git a/sizzle/utils/__init__.py b/sizzle/utils/__init__.py new file mode 100644 index 0000000000..c4c5363737 --- /dev/null +++ b/sizzle/utils/__init__.py @@ -0,0 +1,27 @@ +# Make utils a package +import requests + + +def get_github_issue_title(github_issue_url): + """Helper function to fetch the title of a GitHub issue.""" + try: + repo_path = "/".join(github_issue_url.split("/")[3:5]) + issue_number = github_issue_url.split("/")[-1] + github_api_url = f"https://api.github.com/repos/{repo_path}/issues/{issue_number}" + response = requests.get(github_api_url, timeout=10) + if response.status_code == 200: + issue_data = response.json() + return issue_data.get("title", "No Title") + return f"Issue #{issue_number}" + except Exception: + return "No Title" + + +def format_timedelta(td): + """ + Helper function to format timedelta objects into 'Xh Ym Zs' format. + """ + total_seconds = int(td.total_seconds()) + hours, remainder = divmod(total_seconds, 3600) + minutes, seconds = divmod(remainder, 60) + return f"{hours}h {minutes}m {seconds}s" diff --git a/sizzle/utils/model_loader.py b/sizzle/utils/model_loader.py new file mode 100644 index 0000000000..78cecad90c --- /dev/null +++ b/sizzle/utils/model_loader.py @@ -0,0 +1,195 @@ +""" +Helper utilities for loading models dynamically. +This allows Sizzle to work with different Django projects that may have different model structures. +""" +import logging + +from django.apps import apps +from django.core.exceptions import ImproperlyConfigured + +logger = logging.getLogger(__name__) + + +def get_slack_integration_model(): + """ + Get the SlackIntegration model configured in settings. + Returns None if not configured or not available. + """ + from sizzle.conf import SIZZLE_SLACK_INTEGRATION_MODEL + + if not SIZZLE_SLACK_INTEGRATION_MODEL: + return None + + try: + app_label, model_name = SIZZLE_SLACK_INTEGRATION_MODEL.split(".") + return apps.get_model(app_label, model_name) + except (ValueError, LookupError) as e: + logger.warning( + f"SIZZLE_SLACK_INTEGRATION_MODEL refers to model " + f'"{SIZZLE_SLACK_INTEGRATION_MODEL}" that has not been installed. ' + f"Slack integration will be disabled. Error: {e}" + ) + return None + + +def get_organization_model(): + """ + Get the Organization model configured in settings. + Returns None if not configured or not available. + """ + from sizzle.conf import SIZZLE_ORGANIZATION_MODEL + + if not SIZZLE_ORGANIZATION_MODEL: + return None + + try: + app_label, model_name = SIZZLE_ORGANIZATION_MODEL.split(".") + return apps.get_model(app_label, model_name) + except (ValueError, LookupError) as e: + logger.warning( + f"SIZZLE_ORGANIZATION_MODEL refers to model " + f'"{SIZZLE_ORGANIZATION_MODEL}" that has not been installed. ' + f"Organization features will be limited. Error: {e}" + ) + return None + + +def get_userprofile_model(): + """ + Get the UserProfile model configured in settings. + Returns None if not configured or not available. + """ + from sizzle.conf import SIZZLE_USERPROFILE_MODEL + + if not SIZZLE_USERPROFILE_MODEL: + return None + + try: + app_label, model_name = SIZZLE_USERPROFILE_MODEL.split(".") + return apps.get_model(app_label, model_name) + except (ValueError, LookupError) as e: + logger.warning( + f"SIZZLE_USERPROFILE_MODEL refers to model " + f'"{SIZZLE_USERPROFILE_MODEL}" that has not been installed. ' + f"User profile features will be limited. Error: {e}" + ) + return None + + +def get_notification_model(): + """ + Get the Notification model configured in settings. + Returns None if not configured or not available. + """ + from sizzle.conf import SIZZLE_NOTIFICATION_MODEL + + if not SIZZLE_NOTIFICATION_MODEL: + return None + + try: + app_label, model_name = SIZZLE_NOTIFICATION_MODEL.split(".") + return apps.get_model(app_label, model_name) + except (ValueError, LookupError) as e: + logger.warning( + f"SIZZLE_NOTIFICATION_MODEL refers to model " + f'"{SIZZLE_NOTIFICATION_MODEL}" that has not been installed. ' + f"Notification features will be disabled. Error: {e}" + ) + return None + + +def get_reminder_settings_model(): + """ + Get the ReminderSettings model from sizzle app. + This is internal to sizzle and should always be available. + """ + try: + return apps.get_model("sizzle", "ReminderSettings") + except LookupError as e: + raise ImproperlyConfigured( + f"Could not load ReminderSettings model from sizzle app. " + f"Make sure sizzle migrations have been run. Error: {e}" + ) + + +def get_timelog_model(): + """ + Get the TimeLog model from sizzle app. + This is internal to sizzle and should always be available. + """ + try: + return apps.get_model("sizzle", "TimeLog") + except LookupError as e: + raise ImproperlyConfigured( + f"Could not load TimeLog model from sizzle app. " f"Make sure sizzle migrations have been run. Error: {e}" + ) + + +def get_daily_status_report_model(): + """ + Get the DailyStatusReport model from sizzle app. + This is internal to sizzle and should always be available. + """ + try: + return apps.get_model("sizzle", "DailyStatusReport") + except LookupError as e: + raise ImproperlyConfigured( + f"Could not load DailyStatusReport model from sizzle app. " + f"Make sure sizzle migrations have been run. Error: {e}" + ) + + +def check_slack_dependencies(): + """ + Check if Slack dependencies are available. + Returns tuple (is_available, error_message) + """ + try: + from slack_bolt import App # noqa + from slack_sdk.web import WebClient # noqa + + return True, None + except ImportError as e: + return False, f"slack-bolt not installed. Install with: pip install slack-bolt. Error: {e}" + + +def validate_model_configuration(): + """ + Validate that all required models are properly configured and available. + Returns a dictionary with model availability status. + """ + from sizzle.conf import SIZZLE_DAILY_CHECKINS_ENABLED, SIZZLE_EMAIL_REMINDERS_ENABLED, SIZZLE_SLACK_ENABLED + + status = { + "slack_integration": None, + "organization": None, + "userprofile": None, + "notification": None, + "reminder_settings": None, + "timelog": None, + "slack_deps": None, + } + + # Check core sizzle models + try: + status["reminder_settings"] = get_reminder_settings_model() is not None + status["timelog"] = get_timelog_model() is not None + except ImproperlyConfigured: + status["reminder_settings"] = False + status["timelog"] = False + + # Check optional models + if SIZZLE_SLACK_ENABLED: + status["slack_integration"] = get_slack_integration_model() is not None + slack_available, _ = check_slack_dependencies() + status["slack_deps"] = slack_available + + if SIZZLE_EMAIL_REMINDERS_ENABLED or SIZZLE_DAILY_CHECKINS_ENABLED: + status["userprofile"] = get_userprofile_model() is not None + + if SIZZLE_DAILY_CHECKINS_ENABLED: + status["notification"] = get_notification_model() is not None + + status["organization"] = get_organization_model() is not None + + return status diff --git a/sizzle/views.py b/sizzle/views.py new file mode 100644 index 0000000000..a6e2a5467b --- /dev/null +++ b/sizzle/views.py @@ -0,0 +1,367 @@ +import logging +from collections import defaultdict +from datetime import datetime, timedelta + +from django.contrib import messages +from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from django.db.models import Sum +from django.http import HttpResponseBadRequest, HttpResponseForbidden, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.dateparse import parse_datetime +from django.utils.timezone import now +from rest_framework import status +from rest_framework.authtoken.models import Token + +from sizzle.utils import format_timedelta, get_github_issue_title +from sizzle.utils.model_loader import get_daily_status_report_model, get_organization_model, get_timelog_model + +logger = logging.getLogger(__name__) + + +def sizzle_docs(request): + return render(request, "sizzle_docs.html") + + +def sizzle(request): + # Get models dynamically + TimeLog = get_timelog_model() + + # Aggregate leaderboard data: username and total_duration + leaderboard_qs = ( + TimeLog.objects.values("user__username").annotate(total_duration=Sum("duration")).order_by("-total_duration") + ) + + # Process leaderboard to include formatted_duration + leaderboard = [] + for entry in leaderboard_qs: + username = entry["user__username"] + total_duration = entry["total_duration"] or timedelta() # Handle None + formatted_duration = format_timedelta(total_duration) + leaderboard.append( + { + "username": username, + "formatted_duration": formatted_duration, + } + ) + + # Initialize sizzle_data + sizzle_data = None + + if request.user.is_authenticated: + last_data = TimeLog.objects.filter(user=request.user).order_by("-created").first() + + if last_data: + all_data = TimeLog.objects.filter(user=request.user, created__date=last_data.created.date()).order_by( + "created" + ) + + total_duration = sum((entry.duration for entry in all_data if entry.duration), timedelta()) + + formatted_duration = format_timedelta(total_duration) + + github_issue_url = all_data.first().github_issue_url + issue_title = get_github_issue_title(github_issue_url) + + start_time = all_data.first().start_time.strftime("%I:%M %p") + date = last_data.created.strftime("%d %B %Y") + + sizzle_data = { + "id": last_data.id, + "issue_title": issue_title, + "duration": formatted_duration, + "start_time": start_time, + "date": date, + } + + return render( + request, + "sizzle.html", + {"sizzle_data": sizzle_data, "leaderboard": leaderboard}, + ) + + +def checkIN(request): + from datetime import date + + # Get models dynamically + DailyStatusReport = get_daily_status_report_model() + + # Find the most recent date that has data + last_report = DailyStatusReport.objects.order_by("-date").first() + if last_report: + default_start_date = last_report.date + default_end_date = last_report.date + else: + # If no data at all, fallback to today + default_start_date = date.today() + default_end_date = date.today() + + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") + + if start_date_str and end_date_str: + try: + start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() + end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() + except ValueError: + start_date = default_start_date + end_date = default_end_date + else: + # No date range provided, use the default (most recent date with data) + start_date = default_start_date + end_date = default_end_date + + reports = ( + DailyStatusReport.objects.filter(date__range=(start_date, end_date)) + .select_related("user") + .order_by("date", "created") + ) + + data = [] + for r in reports: + data.append( + { + "id": r.id, + "username": r.user.username, + "previous_work": truncate_text(r.previous_work), + "next_plan": truncate_text(r.next_plan), + "blockers": truncate_text(r.blockers), + "goal_accomplished": r.goal_accomplished, # Add this line + "current_mood": r.current_mood, # Add this line + "date": r.date.strftime("%d %B %Y"), + } + ) + + if request.headers.get("X-Requested-With") == "XMLHttpRequest": + return JsonResponse(data, safe=False) + + # Render template with initial data if needed + return render( + request, + "checkin.html", + { + "data": data, + "default_start_date": default_start_date.isoformat(), + "default_end_date": default_end_date.isoformat(), + }, + ) + + +def truncate_text(text, length=15): + return text if len(text) <= length else text[:length] + "..." + + +@login_required +def add_sizzle_checkIN(request): + # Get models dynamically + DailyStatusReport = get_daily_status_report_model() + + # Fetch yesterday's report + yesterday = now().date() - timedelta(days=1) + yesterday_report = DailyStatusReport.objects.filter(user=request.user, date=yesterday).first() + + # Fetch all check-ins for the user, ordered by date + all_checkins = DailyStatusReport.objects.filter(user=request.user).order_by("-date") + + return render( + request, + "add_sizzle_checkin.html", + {"yesterday_report": yesterday_report, "all_checkins": all_checkins}, + ) + + +@login_required +def checkIN_detail(request, report_id): + DailyStatusReport = get_daily_status_report_model() + report = get_object_or_404(DailyStatusReport, pk=report_id) + + # Restrict to own reports or authorized users + if report.user != request.user and not request.user.is_staff: + return HttpResponseForbidden("You don't have permission to view this report.") + + context = { + "username": report.user.username, + "date": report.date.strftime("%d %B %Y"), + "previous_work": report.previous_work, + "next_plan": report.next_plan, + "blockers": report.blockers, + } + return render(request, "checkin_detail.html", context) + + +def TimeLogListAPIView(request): + if not request.user.is_authenticated: + return JsonResponse({"error": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED) + + # Get models dynamically + TimeLog = get_timelog_model() + + start_date_str = request.GET.get("start_date") + end_date_str = request.GET.get("end_date") + + if not start_date_str or not end_date_str: + return JsonResponse( + {"error": "Both start_date and end_date are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + start_date = parse_datetime(start_date_str) + end_date = parse_datetime(end_date_str) + + if not start_date or not end_date: + return JsonResponse({"error": "Invalid date format."}, status=status.HTTP_400_BAD_REQUEST) + + time_logs = TimeLog.objects.filter(user=request.user, created__range=[start_date, end_date]).order_by("created") + + grouped_logs = defaultdict(list) + for log in time_logs: + date_str = log.created.strftime("%Y-%m-%d") + grouped_logs[date_str].append(log) + + response_data = [] + for date, logs in grouped_logs.items(): + first_log = logs[0] + total_duration = sum((log.duration for log in logs if log.duration), timedelta()) + + total_duration_seconds = total_duration.total_seconds() + formatted_duration = f"{int(total_duration_seconds // 60)} min {int(total_duration_seconds % 60)} sec" + + issue_title = get_github_issue_title(first_log.github_issue_url) + + start_time = first_log.start_time.strftime("%I:%M %p") + formatted_date = first_log.created.strftime("%d %B %Y") + + day_data = { + "id": first_log.id, + "issue_title": issue_title, + "duration": formatted_duration, + "start_time": start_time, + "date": formatted_date, + } + + response_data.append(day_data) + + return JsonResponse(response_data, safe=False, status=status.HTTP_200_OK) + + +@login_required +def TimeLogListView(request): + # Get models dynamically + TimeLog = get_timelog_model() + Organization = get_organization_model() + + time_logs = TimeLog.objects.filter(user=request.user).order_by("-start_time") + active_time_log = time_logs.filter(end_time__isnull=True).first() + + # print the all details of the active time log + token, created = Token.objects.get_or_create(user=request.user) + organizations_list = [] + if Organization: + organizations_list_queryset = Organization.objects.all().values("url", "name") + organizations_list = list(organizations_list_queryset) + organization_url = None + if active_time_log and active_time_log.organization: + organization_url = active_time_log.organization.url + return render( + request, + "time_logs.html", + { + "time_logs": time_logs, + "active_time_log": active_time_log, + "token": token.key, + "organizations_list": organizations_list, + "organization_url": organization_url, + }, + ) + + +@login_required +def sizzle_daily_log(request): + # Get models dynamically + DailyStatusReport = get_daily_status_report_model() + + try: + if request.method == "GET": + reports = DailyStatusReport.objects.filter(user=request.user).order_by("-date") + return render(request, "sizzle_daily_status.html", {"reports": reports}) + + if request.method == "POST": + previous_work = request.POST.get("previous_work") + next_plan = request.POST.get("next_plan") + blockers = request.POST.get("blockers") + goal_accomplished = request.POST.get("goal_accomplished") == "on" + current_mood = request.POST.get("feeling") + print(previous_work, next_plan, blockers, goal_accomplished, current_mood) + + DailyStatusReport.objects.create( + user=request.user, + date=now().date(), + previous_work=previous_work, + next_plan=next_plan, + blockers=blockers, + goal_accomplished=goal_accomplished, + current_mood=current_mood, + ) + + messages.success(request, "Daily status report submitted successfully.") + return JsonResponse( + { + "success": "true", + "message": "Daily status report submitted successfully.", + } + ) + + except (ValidationError, IntegrityError) as e: + logger.exception("Error creating daily status report") + messages.error(request, "An error occurred while submitting your report. Please try again.") + return redirect("sizzle") + + return HttpResponseBadRequest("Invalid request method.") + + +@login_required +def user_sizzle_report(request, username): + # Get models dynamically + TimeLog = get_timelog_model() + + user_model = get_user_model() + user = get_object_or_404(user_model, username=username) + time_logs = TimeLog.objects.filter(user=user).order_by("-start_time") + + grouped_logs = defaultdict(list) + for log in time_logs: + date_str = log.created.strftime("%Y-%m-%d") + grouped_logs[date_str].append(log) + + response_data = [] + for date, logs in grouped_logs.items(): + first_log = logs[0] + total_duration = sum((log.duration for log in logs if log.duration), timedelta()) + + total_duration_seconds = total_duration.total_seconds() + formatted_duration = f"{int(total_duration_seconds // 60)} min {int(total_duration_seconds % 60)} sec" + + issue_title = get_github_issue_title(first_log.github_issue_url) + + start_time = first_log.start_time.strftime("%I:%M %p") + end_time = first_log.end_time.strftime("%I:%M %p") if first_log.end_time else "In Progress" + formatted_date = first_log.created.strftime("%d %B %Y") + + day_data = { + "issue_title": issue_title, + "duration": formatted_duration, + "start_time": start_time, + "end_time": end_time, + "date": formatted_date, + } + + response_data.append(day_data) + + return render( + request, + "user_sizzle_report.html", + {"response_data": response_data, "user": user}, + ) diff --git a/website/management/commands/run_daily.py b/website/management/commands/run_daily.py index b6da899bfa..4849a08a99 100644 --- a/website/management/commands/run_daily.py +++ b/website/management/commands/run_daily.py @@ -42,13 +42,10 @@ def handle(self, *args, **options): except Exception as e: logger.error("Error fetching GSoC PRs", exc_info=True) try: - call_command("daily_checkin_reminder") + # Run all sizzle-related daily tasks + call_command("run_sizzle_daily") except Exception as e: - logger.error("Error sending daily checkin reminders", exc_info=True) - try: - call_command("cron_send_reminders") - except Exception as e: - logger.error("Error sending user reminders", exc_info=True) + logger.error("Error running Sizzle daily tasks", exc_info=True) except Exception as e: logger.error("Error in daily tasks", exc_info=True) raise diff --git a/website/views/organization.py b/website/views/organization.py index 1b66329313..9706ea7f15 100644 --- a/website/views/organization.py +++ b/website/views/organization.py @@ -3,7 +3,6 @@ import logging import re import time -from collections import defaultdict from datetime import datetime, timedelta from decimal import Decimal from urllib.parse import urlparse @@ -19,32 +18,21 @@ from django.core.mail import send_mail from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db.models import Count, Prefetch, Q, Sum -from django.http import ( - Http404, - HttpResponse, - HttpResponseBadRequest, - HttpResponseRedirect, - JsonResponse, - StreamingHttpResponse, -) +from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse, StreamingHttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils import timezone -from django.utils.dateparse import parse_datetime from django.utils.decorators import method_decorator from django.utils.text import slugify from django.utils.timezone import now from django.views.decorators.http import require_POST from django.views.generic import DetailView, FormView, ListView, TemplateView, View from django.views.generic.edit import CreateView -from rest_framework import status -from rest_framework.authtoken.models import Token from website.forms import CaptchaForm, HuntForm, IpReportForm, RoomForm, UserProfileForm from website.models import ( IP, Activity, - DailyStatusReport, Domain, GitHubIssue, Hunt, @@ -65,7 +53,7 @@ Winner, ) from website.services.blue_sky_service import BlueSkyService -from website.utils import format_timedelta, get_client_ip, get_github_issue_title, rebuild_safe_url, validate_file_type +from website.utils import get_client_ip, rebuild_safe_url, validate_file_type logger = logging.getLogger(__name__) @@ -1118,223 +1106,6 @@ def post(self, request, *args, **kwargs): return HttpResponse(f"Error: {str(e)}") -@login_required -def user_sizzle_report(request, username): - user = get_object_or_404(User, username=username) - time_logs = TimeLog.objects.filter(user=user).order_by("-start_time") - - grouped_logs = defaultdict(list) - for log in time_logs: - date_str = log.created.strftime("%Y-%m-%d") - grouped_logs[date_str].append(log) - - response_data = [] - for date, logs in grouped_logs.items(): - first_log = logs[0] - total_duration = sum((log.duration for log in logs if log.duration), timedelta()) - - total_duration_seconds = total_duration.total_seconds() - formatted_duration = f"{int(total_duration_seconds // 60)} min {int(total_duration_seconds % 60)} sec" - - issue_title = get_github_issue_title(first_log.github_issue_url) - - start_time = first_log.start_time.strftime("%I:%M %p") - end_time = first_log.end_time.strftime("%I:%M %p") - formatted_date = first_log.created.strftime("%d %B %Y") - - day_data = { - "issue_title": issue_title, - "duration": formatted_duration, - "start_time": start_time, - "end_time": end_time, - "date": formatted_date, - } - - response_data.append(day_data) - - return render( - request, - "sizzle/user_sizzle_report.html", - {"response_data": response_data, "user": user}, - ) - - -@login_required -def sizzle_daily_log(request): - try: - if request.method == "GET": - reports = DailyStatusReport.objects.filter(user=request.user).order_by("-date") - return render(request, "sizzle/sizzle_daily_status.html", {"reports": reports}) - - if request.method == "POST": - previous_work = request.POST.get("previous_work") - next_plan = request.POST.get("next_plan") - blockers = request.POST.get("blockers") - goal_accomplished = request.POST.get("goal_accomplished") == "on" - current_mood = request.POST.get("feeling") - print(previous_work, next_plan, blockers, goal_accomplished, current_mood) - - DailyStatusReport.objects.create( - user=request.user, - date=now().date(), - previous_work=previous_work, - next_plan=next_plan, - blockers=blockers, - goal_accomplished=goal_accomplished, - current_mood=current_mood, - ) - - messages.success(request, "Daily status report submitted successfully.") - return JsonResponse( - { - "success": "true", - "message": "Daily status report submitted successfully.", - } - ) - - except Exception as e: - messages.error(request, f"An error occurred: {e}") - return redirect("sizzle") - - return HttpResponseBadRequest("Invalid request method.") - - -@login_required -def TimeLogListView(request): - time_logs = TimeLog.objects.filter(user=request.user).order_by("-start_time") - active_time_log = time_logs.filter(end_time__isnull=True).first() - - # print the all details of the active time log - token, created = Token.objects.get_or_create(user=request.user) - organizations_list_queryset = Organization.objects.all().values("url", "name") - organizations_list = list(organizations_list_queryset) - organization_url = None - if active_time_log and active_time_log.organization: - organization_url = active_time_log.organization.url - return render( - request, - "sizzle/time_logs.html", - { - "time_logs": time_logs, - "active_time_log": active_time_log, - "token": token.key, - "organizations_list": organizations_list, - "organization_url": organization_url, - }, - ) - - -def TimeLogListAPIView(request): - if not request.user.is_authenticated: - return JsonResponse({"error": "Unauthorized"}, status=status.HTTP_401_UNAUTHORIZED) - - start_date_str = request.GET.get("start_date") - end_date_str = request.GET.get("end_date") - - if not start_date_str or not end_date_str: - return JsonResponse( - {"error": "Both start_date and end_date are required."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - start_date = parse_datetime(start_date_str) - end_date = parse_datetime(end_date_str) - - if not start_date or not end_date: - return JsonResponse({"error": "Invalid date format."}, status=status.HTTP_400_BAD_REQUEST) - - time_logs = TimeLog.objects.filter(user=request.user, created__range=[start_date, end_date]).order_by("created") - - grouped_logs = defaultdict(list) - for log in time_logs: - date_str = log.created.strftime("%Y-%m-%d") - grouped_logs[date_str].append(log) - - response_data = [] - for date, logs in grouped_logs.items(): - first_log = logs[0] - total_duration = sum((log.duration for log in logs if log.duration), timedelta()) - - total_duration_seconds = total_duration.total_seconds() - formatted_duration = f"{int(total_duration_seconds // 60)} min {int(total_duration_seconds % 60)} sec" - - issue_title = get_github_issue_title(first_log.github_issue_url) - - start_time = first_log.start_time.strftime("%I:%M %p") - formatted_date = first_log.created.strftime("%d %B %Y") - - day_data = { - "id": first_log.id, - "issue_title": issue_title, - "duration": formatted_duration, - "start_time": start_time, - "date": formatted_date, - } - - response_data.append(day_data) - - return JsonResponse(response_data, safe=False, status=status.HTTP_200_OK) - - -def sizzle_docs(request): - return render(request, "sizzle/sizzle_docs.html") - - -def sizzle(request): - # Aggregate leaderboard data: username and total_duration - leaderboard_qs = ( - TimeLog.objects.values("user__username").annotate(total_duration=Sum("duration")).order_by("-total_duration") - ) - - # Process leaderboard to include formatted_duration - leaderboard = [] - for entry in leaderboard_qs: - username = entry["user__username"] - total_duration = entry["total_duration"] or timedelta() # Handle None - formatted_duration = format_timedelta(total_duration) - leaderboard.append( - { - "username": username, - "formatted_duration": formatted_duration, - } - ) - - # Initialize sizzle_data - sizzle_data = None - - if request.user.is_authenticated: - last_data = TimeLog.objects.filter(user=request.user).order_by("-created").first() - - if last_data: - all_data = TimeLog.objects.filter(user=request.user, created__date=last_data.created.date()).order_by( - "created" - ) - - total_duration = sum((entry.duration for entry in all_data if entry.duration), timedelta()) - - formatted_duration = format_timedelta(total_duration) - - github_issue_url = all_data.first().github_issue_url - issue_title = get_github_issue_title(github_issue_url) - - start_time = all_data.first().start_time.strftime("%I:%M %p") - date = last_data.created.strftime("%d %B %Y") - - sizzle_data = { - "id": last_data.id, - "issue_title": issue_title, - "duration": formatted_duration, - "start_time": start_time, - "date": date, - } - - return render( - request, - "sizzle/sizzle.html", - {"sizzle_data": sizzle_data, "leaderboard": leaderboard}, - ) - - def trademark_detailview(request, slug): if settings.USPTO_API is None: return HttpResponse("API KEY NOT SETUP") @@ -1948,102 +1719,6 @@ def approve_activity(request, id): return JsonResponse({"success": False, "error": "Not authorized"}) -def truncate_text(text, length=15): - return text if len(text) <= length else text[:length] + "..." - - -@login_required -def add_sizzle_checkIN(request): - # Fetch yesterday's report - yesterday = now().date() - timedelta(days=1) - yesterday_report = DailyStatusReport.objects.filter(user=request.user, date=yesterday).first() - - # Fetch all check-ins for the user, ordered by date - all_checkins = DailyStatusReport.objects.filter(user=request.user).order_by("-date") - - return render( - request, - "sizzle/add_sizzle_checkin.html", - {"yesterday_report": yesterday_report, "all_checkins": all_checkins}, - ) - - -def checkIN(request): - from datetime import date - - # Find the most recent date that has data - last_report = DailyStatusReport.objects.order_by("-date").first() - if last_report: - default_start_date = last_report.date - default_end_date = last_report.date - else: - # If no data at all, fallback to today - default_start_date = date.today() - default_end_date = date.today() - - start_date_str = request.GET.get("start_date") - end_date_str = request.GET.get("end_date") - - if start_date_str and end_date_str: - try: - start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() - end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() - except ValueError: - start_date = default_start_date - end_date = default_end_date - else: - # No date range provided, use the default (most recent date with data) - start_date = default_start_date - end_date = default_end_date - - reports = ( - DailyStatusReport.objects.filter(date__range=(start_date, end_date)) - .select_related("user") - .order_by("date", "created") - ) - - data = [] - for r in reports: - data.append( - { - "id": r.id, - "username": r.user.username, - "previous_work": truncate_text(r.previous_work), - "next_plan": truncate_text(r.next_plan), - "blockers": truncate_text(r.blockers), - "goal_accomplished": r.goal_accomplished, # Add this line - "current_mood": r.current_mood, # Add this line - "date": r.date.strftime("%d %B %Y"), - } - ) - - if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return JsonResponse(data, safe=False) - - # Render template with initial data if needed - return render( - request, - "sizzle/checkin.html", - { - "data": data, - "default_start_date": default_start_date.isoformat(), - "default_end_date": default_end_date.isoformat(), - }, - ) - - -def checkIN_detail(request, report_id): - report = get_object_or_404(DailyStatusReport, pk=report_id) - context = { - "username": report.user.username, - "date": report.date.strftime("%d %B %Y"), - "previous_work": report.previous_work, - "next_plan": report.next_plan, - "blockers": report.blockers, - } - return render(request, "sizzle/checkin_detail.html", context) - - class RoomsListView(ListView): model = Room template_name = "rooms_list.html"