Skip to content

Posting-2: Base of posting functionality#132

Open
mlv-dev wants to merge 108 commits intoposting.finalfrom
posting.2-base_posting
Open

Posting-2: Base of posting functionality#132
mlv-dev wants to merge 108 commits intoposting.finalfrom
posting.2-base_posting

Conversation

@mlv-dev
Copy link
Copy Markdown
Collaborator

@mlv-dev mlv-dev commented Aug 26, 2025

Summary by CodeRabbit

  • New Features
    • Social publishing: add and manage network accounts (Twitter, Telegram, VK) and create scheduled posts with per-account forms.
    • Rich per-network editor: custom toolbars, tweet-splitting, image handling and CKEditor integration.
    • Admin improvements: multi-account scheduling page, list action to schedule posts, improved status display and readonly behavior.
    • Scheduling backend: queueing, execution and status hooks for planned posts.
    • Browser timezone detection to show admin dates/times in the user’s local zone.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 26, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbit review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds a new publisher Django app with social-account models (Twitter/Telegram/VK), account registry, planned and regular post models, Django Q scheduling and hooks, admin integrations and forms, CKEditor editor wiring and editor JS, timezone detection middleware and client script, initial migration, and an admin schedule-posts UI.

Changes

Cohort / File(s) Summary
Frontend timezone detection
static/js/detect_timezone.js, templates/admin/base_site.html
New deferred script detects browser IANA timezone and persists it to a user_timezone cookie; admin base template includes the script in extrahead.
Planned post editor & schedule template
static/js/planned_post_editor.js, publisher/templates/schedule_posts.html
Adds JS to initialize and reconfigure CKEditor per-account, clean tweet meta, and rebuild editors on account changes; adds admin template rendering per-account scheduling forms and including CKEditor media.
App config, admin registration & migration
publisher/apps.py, publisher/admin.py, publisher/migrations/0001_initial.py
Adds PublisherConfig AppConfig, admin registration module, and initial migration creating Account, PlannedPost, RegularPost, and concrete social account models (Twitter/Telegram/VK) with related fields and choices.
Account model & admin workflows
publisher/account.py
Introduces Account (GenericForeignKey to social object), AccountForm, and AccountAdmin with per-network add/change redirects, an action to schedule posts, custom get_urls, and schedule_posts_view implementing multi-account scheduling and transactional PlannedPost creation/scheduling.
Planned posting model, form & admin
publisher/planned_post.py
Adds PlannedPost model, PlannedPostForm, and PlannedPostAdmin; implements validation (no past datetimes), localtime handling, CKEditor integration, schedule/task linkage, save/delete hooks, and admin display/readonly logic.
Scheduler (Django Q) integration
publisher/scheduler.py
Adds schedule_task, publish_post, and status_hook to create Django Q one-off schedules/tasks for PlannedPost publishing and update statuses via hooks; uses local imports to avoid circular dependencies.
Social integration base & registry
publisher/socials/base.py, publisher/socials/socials.py
Adds abstract SocialNetworkBase (shared CKEditor config and helpers), SocialNetworkAdminBase and HiddenModelAdmin, a socials registry, TYPE_CHOICES, and CKEDITOR_CONFIGS.
Twitter integration
publisher/socials/twitter.py
Adds TwitterAccount model with encrypted credentials, extended CKEditor config (tweet splitter), content cleaning into tweet segments and images, threaded tweet posting via Tweepy (with media upload), form, and admin registration.
Telegram integration
publisher/socials/telegram.py
Adds TelegramAccount model with encrypted token/channel_id, content cleaning (extract image), posting via python-telegram-bot, form, and admin registration.
VK integration
publisher/socials/vk.py
Adds VkAccount model with encrypted credentials and community_id, content cleaning, posting via vk_api (upload + group post), form, and admin registration.
RegularPost placeholder
publisher/regular_post.py
Adds minimal RegularPost model with verbose names and __str__.
Middleware: timezone override
publisher/middleware.py
Adds TimezoneMiddleware that reads user_timezone cookie, resolves a ZoneInfo (falls back to settings.TIME_ZONE), and applies timezone.override during request processing.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Browser
  participant detectJS as detect_timezone.js
  participant Server as Django App
  participant Middleware as TimezoneMiddleware

  Note over Browser: Admin page load
  Browser->>detectJS: run IIFE -> detect IANA TZ
  detectJS-->>Browser: set `user_timezone` cookie (secure when https)
  Browser->>Server: HTTP request (cookie included)
  Server->>Middleware: __call__(request)
  Middleware->>Middleware: read/unquote cookie -> ZoneInfo (fallback)
  Middleware->>Server: timezone.override(...) then call view
  Server-->>Browser: Response
Loading
sequenceDiagram
  autonumber
  actor Admin
  participant AdminUI as Django Admin (Accounts)
  participant SchedView as schedule_posts_view
  participant Scheduler as publisher.scheduler
  participant Q as Django Q
  participant Worker as Q Worker
  participant Social as Account.social.post

  Admin->>AdminUI: Click "Schedule posts" action
  AdminUI->>SchedView: GET -> render per-account forms
  Admin->>SchedView: POST forms
  SchedView->>Scheduler: for each valid form -> create PlannedPost and call schedule_task()
  Scheduler->>Q: create one-off schedule for publish_post(post_id) with hook
  SchedView-->>AdminUI: redirect to PlannedPost changelist

  Note over Q,Worker: At scheduled time
  Q->>Worker: run publish_post(post_id)
  Worker->>Scheduler: publish_post(post_id)
  Scheduler->>Social: post(planned_post)
  alt social returns success
    Scheduler->>PlannedPost: set status='success'
  else returns "[ERROR]..."
    Scheduler->>PlannedPost: set status='caughtError'
  end
  Worker->>Scheduler: run status_hook(task)
  alt task.failed
    Scheduler->>PlannedPost: set status='uncaughtError'
  end
Loading
sequenceDiagram
  autonumber
  actor Admin
  participant SNAdmin as SocialNetworkAdminBase (e.g., TwitterAdmin)
  participant Accounts as AccountAdmin

  Admin->>SNAdmin: Add social instance (credentials)
  SNAdmin->>SNAdmin: response_add() creates Account linking to social instance
  SNAdmin-->>Admin: redirect to Accounts changelist
Loading
sequenceDiagram
  autonumber
  actor Admin
  participant Page as schedule_posts.html
  participant JS as planned_post_editor.js
  participant CK as CKEditor

  Admin->>Page: open schedule page
  Page-->>JS: DOMContentLoaded
  JS->>JS: parse per-account CKEditor configs
  JS->>CK: initialize editor(s) for textarea(s)
  Admin->>Page: change account select (single-account edit)
  JS->>CK: destroy & recreate editor with new config
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch posting.2-base_posting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@mlv-dev
Copy link
Copy Markdown
Collaborator Author

mlv-dev commented Aug 26, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 26, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (32)
posting/regular_post.py (2)

3-6: Add str and correct Russian plural form for admin readability.

Without str, admin lists will show “RegularPost object (id)”. Also, verbose_name_plural should be plural in Russian.

Apply:

 class RegularPost(models.Model):
     class Meta:
-        verbose_name = 'Регулярный постинг'
-        verbose_name_plural = 'Регулярный постинг'
+        verbose_name = 'Регулярный постинг'
+        verbose_name_plural = 'Регулярные постинги'
+
+    def __str__(self) -> str:
+        return f'Регулярный постинг #{self.pk}'

3-6: Consider adding ordering or timestamps if this model will list/schedule posts.

Even as a base stub, created_at/updated_at and default ordering reduce future migrations and admin clutter.

If desired:

 class RegularPost(models.Model):
+    created_at = models.DateTimeField(auto_now_add=True)
+    updated_at = models.DateTimeField(auto_now=True)
     class Meta:
         verbose_name = 'Регулярный постинг'
         verbose_name_plural = 'Регулярные постинги'
+        ordering = ('-created_at',)
templates/admin/base_site.html (1)

4-7: Load timezone script with defer to avoid blocking rendering.

Cookie affects subsequent requests, so deferring is safe and non-blocking.

 {% block extrahead %}
     {{ block.super }}
-    <script src="{% static 'js/detect_timezone.js' %}"></script>
+    <script defer src="{% static 'js/detect_timezone.js' %}"></script>
 {% endblock %}
static/js/detect_timezone.js (2)

1-6: Harden cookie write: encode value, set SameSite, Secure (when HTTPS), and persistence.

Prevents odd chars issues, improves security, and keeps the cookie across browser restarts.

 (function() {
     const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
     if (tz) {
-        document.cookie = "user_timezone=" + tz + "; path=/";
+        const attrs = ["path=/", "SameSite=Lax", "Max-Age=31536000"]; // 1 year
+        if (location.protocol === "https:") attrs.push("Secure");
+        document.cookie = `user_timezone=${encodeURIComponent(tz)}; ${attrs.join("; ")}`;
     }
 })();

2-5: Avoid unnecessary rewrites when cookie already matches.

Minor perf/read-write reduction.

-    const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
-    if (tz) {
-        const attrs = ["path=/", "SameSite=Lax", "Max-Age=31536000"]; // 1 year
-        if (location.protocol === "https:") attrs.push("Secure");
-        document.cookie = `user_timezone=${encodeURIComponent(tz)}; ${attrs.join("; ")}`;
-    }
+    const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
+    if (!tz) return;
+    const current = document.cookie.split("; ").find(s => s.startsWith("user_timezone="))?.split("=")[1];
+    if (current === encodeURIComponent(tz)) return;
+    const attrs = ["path=/", "SameSite=Lax", "Max-Age=31536000"];
+    if (location.protocol === "https:") attrs.push("Secure");
+    document.cookie = `user_timezone=${encodeURIComponent(tz)}; ${attrs.join("; ")}`;
posting/apps.py (1)

3-5: Set default_auto_field to avoid Django warnings and ensure BigAutoField IDs.

Keeps migrations stable across environments.

 class PostingConfig(AppConfig):
     name = 'posting'
     verbose_name = 'Постинг'
+    default_auto_field = 'django.db.models.BigAutoField'
posting/middleware.py (3)

8-13: Prefer try/except over linear membership check and use settings.TIME_ZONE as fallback.

Membership against pytz.all_timezones is O(N) and unnecessary. Also, using project default avoids hardcoding UTC.

Minimal change, keeping pytz:

-from django.utils import timezone
-import pytz
+from django.utils import timezone
+from django.conf import settings
+import pytz
+from pytz import UnknownTimeZoneError

 class TimezoneMiddleware:
@@
     def __call__(self, request):
-        tzname = request.COOKIES.get('user_timezone', 'UTC')
-        if tzname in pytz.all_timezones:
-            timezone.activate(pytz.timezone(tzname))
-        else:
-            timezone.activate(pytz.UTC)
+        tzname = request.COOKIES.get('user_timezone', settings.TIME_ZONE)
+        try:
+            tz = pytz.timezone(tzname)
+        except UnknownTimeZoneError:
+            tz = pytz.timezone(settings.TIME_ZONE)
+        timezone.activate(tz)
         response = self.get_response(request)
         return response

1-15: Modernize to zoneinfo (Django ≥4) and drop pytz (optional).

Aligns with current Django guidance and removes a dependency. If your runtime is Python ≥3.9.

-from django.utils import timezone
-from django.conf import settings
-import pytz
-from pytz import UnknownTimeZoneError
+from django.utils import timezone
+from django.conf import settings
+from zoneinfo import ZoneInfo

 class TimezoneMiddleware:
@@
     def __call__(self, request):
-        tzname = request.COOKIES.get('user_timezone', settings.TIME_ZONE)
-        try:
-            tz = pytz.timezone(tzname)
-        except UnknownTimeZoneError:
-            tz = pytz.timezone(settings.TIME_ZONE)
-        timezone.activate(tz)
+        tzname = request.COOKIES.get('user_timezone', settings.TIME_ZONE)
+        try:
+            tz = ZoneInfo(tzname)
+        except Exception:
+            tz = ZoneInfo(settings.TIME_ZONE)
+        timezone.activate(tz)
         response = self.get_response(request)
         return response

8-15: Optional: Deactivate timezone after response to minimize leakage across long-lived threads.

Django uses thread-local; deactivation is defensive and low-cost.

     def __call__(self, request):
         tzname = request.COOKIES.get('user_timezone', settings.TIME_ZONE)
         try:
             tz = ZoneInfo(tzname)
         except Exception:
             tz = ZoneInfo(settings.TIME_ZONE)
-        timezone.activate(tz)
-        response = self.get_response(request)
-        return response
+        timezone.activate(tz)
+        try:
+            return self.get_response(request)
+        finally:
+            timezone.deactivate()

(If you keep pytz, the try/finally structure applies the same.)

posting/templates/schedule_posts.html (3)

12-14: Consider enctype for potential file widgets

If any of the embedded forms include file inputs or widgets that rely on form submission (not CKEditor uploads), the form must declare enctype="multipart/form-data". If not needed, you can ignore.

Would any of the PlannedPost forms (now or soon) submit files via the form? If yes, apply:

-  <form method="post">{% csrf_token %}
+  <form method="post" enctype="multipart/form-data">{% csrf_token %}

15-15: Internationalize hardcoded labels

"Account" and the submit button should use i18n so the admin UI stays consistent with LANGUAGE_CODE and LANGUAGES.

Apply this diff:

-      <thead><tr><th>Account</th><th>Форма</th></tr></thead>
+      <thead><tr><th>{% trans "Account" %}</th><th>{% trans "Form" %}</th></tr></thead>
-    <input type="submit" class="default" value="Создать запланированные посты">
+    <input type="submit" class="default" value="{% trans "Create scheduled posts" %}">

Also applies to: 30-30


17-24: Avoid coupling JS to rendered text; expose an explicit config key

planned_post_editor.js derives the CKEditor config key from cell text. Text is brittle (localization, formatting). Prefer a data attribute to carry the exact key the JS needs.

Apply this diff to expose a stable key on the row (example uses the current display value; swap to a slug/constant if available):

-      {% for account, post_form in form_objects %}
-        <tr>
+      {% for account, post_form in form_objects %}
+        <tr data-ckeditor-config-key="{{ account.get_social_network_display }}">
           <td>
             {{ account.title }}<br><small>{{ account.get_social_network_display }}</small>
           </td>

Then in JS, read tr.dataset.ckeditorConfigKey instead of parsing text (see JS review comment for the corresponding change).

Do you have an internal, language-agnostic key for the social network (e.g., "vk", "tg", "ok")? If so, prefer that over the display label.

posting/scheduler.py (4)

4-7: Docstring inaccurately states return type

schedule_task returns the Schedule object (or None), not a task ID string. Adjust the docstring to prevent confusion downstream.

Apply this diff:

 def schedule_task(planned_post):
     """
-    Ставит задачу через django-q на указанное время.
-    Возвращает ID задачи (str).
+    Ставит задачу через django-q на указанное время.
+    Возвращает объект Schedule (или None).
     """

9-16: Optional: capture the created Schedule ID on the PlannedPost

If PlannedPost tracks the schedule entry (distinct from Task ID set in status_hook), consider storing schedule.id here for observability/cancellation. If the model doesn’t have such a field, ignore.

Example:

     task = schedule(
         'posting.scheduler.publish_post', # Путь к функции, которая отправит пост
         planned_post.id,
         schedule_type='O', # One-off
         next_run=planned_post.effective_datetime,
         hook="posting.scheduler.status_hook"
     )
-    return task if task else None
+    if task:
+        # optional: persist schedule id for later management (cancel/reschedule)
+        try:
+            planned_post.schedule_id = task.id  # requires a field
+            planned_post.save(update_fields=['schedule_id'])
+        except Exception:
+            pass
+    return task or None

Does PlannedPost have a field to store a schedule identifier? If yes, I can wire this fully.


18-32: Harden publish flow: log and classify exceptions from social.post

If post.account.social.post raises, Django Q will mark the task failed and status_hook will set "uncaughtError", but the exception isn’t logged with context. Add logging for faster diagnosis. Keep raising to preserve failure semantics.

Apply this diff:

 def publish_post(planned_post_id):
     from .planned_post import PlannedPost
+    import logging
+    logger = logging.getLogger(__name__)
     
     post = PlannedPost.objects.get(pk=planned_post_id)
 
-    result = post.account.social.post(post)
+    try:
+        result = post.account.social.post(post)
+    except Exception as e:
+        logger.exception("Publish failed for PlannedPost id=%s", planned_post_id)
+        # Let Django Q mark the task as failed; status_hook will set 'uncaughtError'
+        raise
     
     if result.startswith("[ERROR]"):
         post.status = 'caughtError'
     else:
         post.status = 'success'
     
     post.save()
 
     return result

34-49: Status hook looks good; minor robustness improvements are optional

  • If you expect concurrent hooks or re-entrancy, consider update_fields=['task_id','status'] on save.
  • If task.args format changes, guard access to task.args[0].

Apply this diff to make args access safer:

-    post_id = task.args[0]  # мы в задачу передали post_id
+    post_id = task.args[0] if getattr(task, 'args', None) else None  # мы в задачу передали post_id
+    if post_id is None:
+        return
dtpstat/settings.py (3)

65-65: Middleware ordering: place TimezoneMiddleware earlier

Timezone activation typically needs to happen before template rendering and often alongside LocaleMiddleware. Consider placing posting.middleware.TimezoneMiddleware right after 'django.middleware.locale.LocaleMiddleware' and before CommonMiddleware.

Apply this diff:

 MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.middleware.locale.LocaleMiddleware',
+    'posting.middleware.TimezoneMiddleware',
     'corsheaders.middleware.CorsMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
-    'posting.middleware.TimezoneMiddleware',
 ]

Confirm the middleware’s dependencies (e.g., SessionMiddleware) so we don’t break assumptions about where it runs.


248-250: Duplicate TIME_ZONE setting

TIME_ZONE is defined earlier (Line 145) and again here (Line 249). Keep a single definition to avoid confusion. USE_TZ here is fine.

Apply this diff:

 USE_TZ = True
-TIME_ZONE = 'UTC'

251-260: Q_CLUSTER defaults look reasonable; add note on production workers

Workers=2 and timeouts are conservative for a first cut. In production, monitor queue latency and adjust workers and recycle accordingly.

Do you plan to run qcluster via a supervisor/systemd unit? If yes, I can provide a hardened unit file and health-check script.

static/js/planned_post_editor.js (8)

1-2: Guard against missing CKEditor

If CKEditor assets fail to load, subsequent calls will throw. Add a quick guard to fail gracefully.

Apply this diff:

 document.addEventListener("DOMContentLoaded", () => {
+  if (!window.CKEDITOR) {
+    console.error('CKEditor not found on page; skipping editor initialization.');
+    return;
+  }

19-21: CKEditor.getData signature: drop unsupported options

CKEditor 4 does not use the { format: 'html' } option; passing it is harmless but noisy. Simplify the call.

Apply this diff:

-    const oldData = instance ? cleanTweetMeta(instance.getData({ format: 'html' })) : (textarea.value || '');
+    const oldData = instance ? cleanTweetMeta(instance.getData()) : (textarea.value || '');

23-29: Avoid blocking alert; use non-blocking logging

A modal alert for each missing config can interrupt multi-row initialization. Prefer console warning and skip the row.

Apply this diff:

-    if (!newConfig) {
-        const error_msg = `CKEditor config для "${accountName}" не найден`;
-        console.error(error_msg);
-        alert(`Ошибка: ${error_msg}`);
-        return; // прекращаем выполнение, чтобы не ломать редактор
-    }
+    if (!newConfig) {
+        const error_msg = `CKEditor config для "${accountName}" не найден`;
+        console.warn(error_msg, { textarea });
+        return; // мягко выходим
+    }

31-39: Don’t clone instance.config; build a clean config

instance.config is a live CKEditor config object and may contain runtime state. Start from a clean object and copy only the needed keys.

Apply this diff:

-    const baseConfig = instance ? Object.assign({}, instance.config) : {};
-    const config = Object.assign({}, baseConfig,
-      newConfig ? {
-        toolbar: newConfig.toolbar,
-        allowedContent: newConfig.allowedContent,
-        extraPlugins: newConfig.extraPlugins
-      } : {}
-    );
+    const config = {
+      // start clean; add only what we truly need per-network
+      toolbar: newConfig.toolbar,
+      allowedContent: newConfig.allowedContent,
+      extraPlugins: newConfig.extraPlugins
+    };

47-47: Optional: validate config before replace

If extraPlugins or toolbar are undefined, CKEditor may warn. Add a lightweight check to aid debugging.

Apply this diff:

-    const newEditor = CKEDITOR.replace(editorId, config);
+    if (!config || typeof config !== 'object') {
+      console.error('Invalid CKEditor config for', editorId, config);
+      return;
+    }
+    const newEditor = CKEDITOR.replace(editorId, config);

76-79: Normalize account key extraction in table mode

In single-form mode you use parseAccountName, but in table mode you take small.textContent as-is. Normalize to reduce drift between UI variants.

Apply this diff:

-    const small = leftTd.querySelector('small');
-    const accountName = small.textContent.trim();
-    restartEditorForTextarea(textarea, accountName, ckConfigs, true);
+    const small = leftTd.querySelector('small');
+    const accountText = (small ? small.textContent : leftTd.textContent || '').trim();
+    const accountKey = parseAccountName(accountText);
+    restartEditorForTextarea(textarea, accountKey, ckConfigs, true);

Alternatively, if you adopt data-ckeditor-config-key on (see template comment), prefer:

const accountKey = tr.dataset.ckeditorConfigKey;

99-106: Defensive read of selected option

selectedOptions may be empty in some browsers/states. Fall back to selectedIndex.

Apply this diff:

-    const apply = () => {
-      const selectedText = select.selectedOptions[0].text;
+    const apply = () => {
+      const opt = select.selectedOptions && select.selectedOptions.length
+        ? select.selectedOptions[0]
+        : select.options[select.selectedIndex];
+      const selectedText = opt ? opt.text : '';
       const accountName = parseAccountName(selectedText);
       restartEditorForTextarea(singleTextarea, accountName, ckConfigs, true);
     };

68-75: JSON parse error path is good; consider early return

If configs JSON is invalid, skip initializing that textarea to avoid partially configured editors.

Apply this diff:

     try {
       ckConfigs = JSON.parse(textarea.dataset.ckeditorconfigs || textarea.dataset.ckeditorConfigs || '{}');
     } catch (e) {
       console.error('Невалидный JSON в data-ckeditor-configs для', textarea, e);
+      return;
     }
posting/planned_post.py (1)

66-66: Remove commented-out code

This commented line appears to be debug code that should be removed.

-            # self.message_user(request, "Ошибка шедулирования", level=messages.ERROR)
posting/account.py (4)

121-121: Remove unused loop variable

The loop variable i is not used within the loop body.

-            for i, acc in enumerate(accounts):
+            for acc in accounts:

136-136: Remove commented-out code

Remove the commented debug line.

-                    # self.message_user(request, "Ошибка шедулирования", level=messages.ERROR)

77-80: Clean up TODO comments

The TODO comment indicates completed work ("[DONE]"). Consider removing or updating this comment.

Either remove the TODO or update it to reflect any remaining work items.


113-113: Optimize database query

The select_related() call without arguments may not optimize the query as intended. Specify the related fields explicitly.

-        accounts = Account.objects.filter(pk__in=pks).select_related()  # оптимизируем
+        accounts = Account.objects.filter(pk__in=pks).select_related('social_type')  # оптимизируем
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 78d3f2d and 0206ea4.

📒 Files selected for processing (13)
  • dtpstat/settings.py (5 hunks)
  • posting/account.py (1 hunks)
  • posting/admin.py (1 hunks)
  • posting/apps.py (1 hunks)
  • posting/middleware.py (1 hunks)
  • posting/migrations/0001_initial.py (1 hunks)
  • posting/planned_post.py (1 hunks)
  • posting/regular_post.py (1 hunks)
  • posting/scheduler.py (1 hunks)
  • posting/templates/schedule_posts.html (1 hunks)
  • static/js/detect_timezone.js (1 hunks)
  • static/js/planned_post_editor.js (1 hunks)
  • templates/admin/base_site.html (1 hunks)
🧰 Additional context used
🪛 Ruff (0.12.2)
posting/account.py

121-121: Loop control variable i not used within loop body

Rename unused i to _i

(B007)

🔇 Additional comments (5)
posting/middleware.py (1)

1-15: Configuration Verified
All required settings are correctly wired:

  • INSTALLED_APPS includes the posting app (posting/apps.py: name = 'posting').
  • MIDDLEWARE contains posting.middleware.TimezoneMiddleware (dtpstat/settings.py, line 65).
  • TIME_ZONE is set to 'UTC' and USE_TZ is True in dtpstat/settings.py (lines 145 & 248).

No further action needed.

posting/admin.py (1)

7-9: LGTM: admin registrations are clear and minimal

Account, PlannedPost, and RegularPost are registered with appropriate admins. No concerns here.

dtpstat/settings.py (3)

49-51: Enable django_q and posting apps

Adds 'django_q' and 'posting' to INSTALLED_APPS. Looks correct and aligns with the scheduler and admin usage.


172-172: CKEDITOR_UPLOAD_PATH change is fine

Switching to 'uploads/' is consistent with MEDIA_ROOT-based storage. Ensure the directory exists in deployed environments.


193-205: CKEditor 'social_networks' profile: ensure it matches client scripts

The new profile looks aligned for compact editing. Verify these keys (toolbar, autoGrow_*, removeFormatTags) match what static/js/planned_post_editor.js expects to inject.

If different per-network toolbars are needed, ensure the serialized JSON includes per-network keys and that the JS selects them correctly.

Comment thread posting/account.py Outdated
Comment thread posting/account.py
Comment thread posting/account.py Outdated
Comment thread posting/migrations/0001_initial.py Outdated
Comment thread posting/migrations/0001_initial.py Outdated
Comment thread posting/planned_post.py Outdated
Comment thread posting/planned_post.py Outdated
Comment thread posting/planned_post.py Outdated
Comment thread posting/planned_post.py Outdated
Comment thread posting/templates/schedule_posts.html Outdated
mlv-dev and others added 2 commits August 27, 2025 02:21
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@mlv-dev mlv-dev force-pushed the posting.2-base_posting branch from 15baadd to 148414e Compare August 26, 2025 23:34
mlv-dev and others added 22 commits August 27, 2025 02:35
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@mlv-dev mlv-dev force-pushed the posting.2-base_posting branch from 97d7e7d to af1a344 Compare August 28, 2025 22:35
@mlv-dev
Copy link
Copy Markdown
Collaborator Author

mlv-dev commented Aug 28, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 28, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

♻️ Duplicate comments (4)
publisher/middleware.py (1)

6-18: Confirm registration/order is handled in PR #131

Per your workflow (learned across your PRs), middleware registration happens in a separate PR. Once #131 is merged, ensure this sits after SessionMiddleware and before LocaleMiddleware.

publisher/socials/twitter.py (1)

133-134: Fix user-facing message (typo/grammar).

Current string is garbled.

-        return self.log("Твит:ы успешно отправлен:ы")
+        return self.log("Твиты успешно отправлены")
publisher/migrations/0001_initial.py (1)

18-18: Do not amend applied initial migrations; prefer additive migrations.

If 0001 was already applied in any environment, corrections (e.g., choice spelling) should come via 0002+ instead of editing 0001 to avoid migration drift. Verify team usage before merging.

publisher/planned_post.py (1)

52-59: *Avoid dereferencing missing related objects; use _id fields.

if self.schedule: and self.schedule can trigger a fetch and raise DoesNotExist before your try/except. Use schedule_id and update/delete by PK.

Apply:

-                if old.datetime_planned != self.datetime_planned and self.schedule:
-                    try:
-                        # Переносим задачу в планировщике
-                        sched = self.schedule
-                        sched.next_run = self.effective_datetime
-                        sched.save(update_fields=['next_run'])
-                    except Exception as e:
-                        raise ValidationError(f"Ошибка при обновлении расписания: {e}")
+                if old.datetime_planned != self.datetime_planned and self.schedule_id:
+                    try:
+                        # Переносим задачу в планировщике без dereference
+                        Schedule.objects.filter(pk=self.schedule_id).update(
+                            next_run=self.effective_datetime
+                        )
+                    except Exception as e:
+                        raise ValidationError(f"Ошибка при обновлении расписания: {e}") from e
-        if self.schedule:
-            try:
-                sched = self.schedule
-                sched.delete()  # удаляем задачу из планировщика
-            except Schedule.DoesNotExist:
-                pass
+        if self.schedule_id:
+            # удаляем задачу из планировщика без dereference
+            Schedule.objects.filter(pk=self.schedule_id).delete()

Also applies to: 70-77

🧹 Nitpick comments (20)
publisher/middleware.py (3)

11-12: Trim decoded cookie to avoid stray whitespace

Occasionally cookies end up with extra spaces; trimming avoids unnecessary fallbacks.

Apply:

-        tzname = unquote(tzname_raw) if tzname_raw else settings.TIME_ZONE
+        tzname = unquote(tzname_raw).strip() if tzname_raw else settings.TIME_ZONE

7-10: Add type hints for clarity and editor support

Annotating the middleware signatures improves readability and tooling.

Apply:

+from typing import Callable
+from django.http import HttpRequest, HttpResponse
@@
-    def __init__(self, get_response):
+    def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
         self.get_response = get_response
@@
-    def __call__(self, request):
+    def __call__(self, request: HttpRequest) -> HttpResponse:

15-16: Log invalid timezone cookie at debug level for diagnostics

Lightweight debug logging helps trace misconfigured clients without spamming warnings.

Apply within the except block:

-        except ZoneInfoNotFoundError:
+        except ZoneInfoNotFoundError:
+            logger.debug(
+                "Invalid timezone cookie %r; falling back to default %s.",
+                tzname,
+                timezone.get_default_timezone_name(),
+            )
             tz = timezone.get_default_timezone()

And add once at module top:

+import logging
+logger = logging.getLogger(__name__)
publisher/scheduler.py (1)

30-36: Condense status assignment and avoid double-branch.

Simpler and matches Ruff hint (SIM114).

-    if not isinstance(result, str):
-        post.status = 'caughtError'
-    elif result.startswith("[ERROR]"):
-        post.status = 'caughtError'
-    else:
-        post.status = 'success'
+    post.status = 'success' if isinstance(result, str) and not result.startswith("[ERROR]") else 'caughtError'
publisher/socials/vk.py (3)

86-90: Use contextlib.suppress for cleanup.

Simplifies error handling; matches Ruff SIM105.

-            if tmp_path and os.path.exists(tmp_path):
-                try:
-                    os.remove(tmp_path)
-                except OSError:
-                    pass
+            if tmp_path and os.path.exists(tmp_path):
+                from contextlib import suppress
+                with suppress(OSError):
+                    os.remove(tmp_path)

30-35: Harden handling (optional).

Support tags without a space after and make removal more robust.

-        img_match = re.search(r'<img [^>]*src="([^"]+)"[^>]*>', content)
+        img_match = re.search(r'<img[^>]*src="([^"]+)"[^>]*>', content)
         photo_src = img_match.group(1) if img_match else None
@@
-        content = re.sub(r'<img [^>]*>', '', content)
+        content = re.sub(r'<img[^>]*>', '', content)

61-69: Bound remote image download (safety).

Current urlretrieve has no size/timeout control; consider streaming with timeouts and a max size to avoid large downloads or slow responses.

I can provide a small helper using requests with timeouts and a size cap if you want it.

publisher/socials/telegram.py (1)

32-44: Optional: more tolerant regex.

Covers <img src="..."> without a space after tag name.

-        img_match = re.search(r'<img [^>]*src="([^"]+)"[^>]*>', content)
+        img_match = re.search(r'<img[^>]*src="([^"]+)"[^>]*>', content)
@@
-        content = re.sub(r'<img [^>]*>', '', content)
+        content = re.sub(r'<img[^>]*>', '', content)
static/js/planned_post_editor.js (1)

36-43: Defensive merge of config.extraPlugins.

Avoid overwriting base extraPlugins if newConfig omits it.

-    const config = Object.assign({}, baseConfig,
-      newConfig ? {
-        toolbar: newConfig.toolbar,
-        allowedContent: newConfig.allowedContent,
-        extraPlugins: newConfig.extraPlugins
-      } : {}
-    );
+    const config = Object.assign({}, baseConfig, {
+      toolbar: newConfig.toolbar,
+      allowedContent: newConfig.allowedContent,
+      extraPlugins: [baseConfig.extraPlugins, newConfig.extraPlugins].filter(Boolean).join(',')
+    });
publisher/socials/twitter.py (1)

32-63: Strip residual HTML before tweeting (optional).

Twitter ignores HTML; leftover tags from the editor may leak into text. Strip tags after your existing removals.

             # 6. Убираем <p> и <br>
             t = re.sub(r'</?p>', '', t)
             t = re.sub(r'<br\s*/?>', '\n', t)
+            # 7. На всякий случай убираем прочие теги
+            t = re.sub(r'<[^>]+>', '', t)
publisher/planned_post.py (5)

59-59: Preserve exception context when re-raising.

Use exception chaining to aid debugging.

Apply:

-                        raise ValidationError(f"Ошибка при обновлении расписания: {e}")
+                        raise ValidationError(f"Ошибка при обновлении расписания: {e}") from e
-                raise ValidationError(f"Ошибка при создании задачи в планировщике: {e}")
+                raise ValidationError(f"Ошибка при создании задачи в планировщике: {e}") from e

Also applies to: 68-68


104-108: Guard against naive datetimes when formatting initial value.

Existing rows might contain naive datetimes; wrap with an awareness check before localtime.

Apply:

-        if self.instance and self.instance.datetime_planned:
-            local_dt = timezone.localtime(self.instance.datetime_planned)    
-            self.initial['datetime_planned'] = local_dt.strftime('%Y-%m-%dT%H:%M') 
+        if self.instance and self.instance.datetime_planned:
+            dt = self.instance.datetime_planned
+            if timezone.is_naive(dt):
+                dt = timezone.make_aware(dt, timezone.get_current_timezone())
+            local_dt = timezone.localtime(dt)
+            self.initial['datetime_planned'] = local_dt.strftime('%Y-%m-%dT%H:%M')

136-141: formfield_overrides doesn’t support form_class; remove it.

form_class is ignored here. Use only the widget override (you already provide a widget in the form Meta).

Apply:

     formfield_overrides = {
         models.DateTimeField: {
             'widget': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
-            'form_class': forms.DateTimeField  # Заставляем использовать DateTimeField
         }
     }

157-157: Unify admin UI language.

clickable_status.short_description should match Russian UI labels.

Apply:

-    clickable_status.short_description = "Status"
+    clickable_status.short_description = "Статус"

175-182: Don’t swallow save errors silently in admin.

Logging plus re-raising gives admin the proper error page; message-only can hide failures in automation. If you prefer non-blocking UX, at least log with traceback.

Apply:

     def save_model(self, request, obj, form, change):
-        try:
-            super().save_model(request, obj, form, change)
-        except Exception as e:
-            self.message_user(
-                request,
-                f"{e}",
-                level=messages.ERROR
-            )
+        import logging
+        logger = logging.getLogger(__name__)
+        try:
+            super().save_model(request, obj, form, change)
+        except Exception as e:
+            logger.exception("Failed to save PlannedPost(id=%s): %s", getattr(obj, 'pk', None), e)
+            self.message_user(request, str(e), level=messages.ERROR)
+            raise

Would you like me to keep the non-blocking behavior (no raise) but still add logging?

publisher/account.py (5)

49-54: Avoid long GET querystrings for IDs; use POST or session.

Large selections can exceed URL limits. Prefer POSTing the IDs or storing them in session before redirect.

I can provide a minimal session-based variant if desired.


64-76: Minor: compute url_name after validation.

Avoid constructing a URL name from unvalidated input.

Apply:

-            url_name = f'admin:publisher_{social_network}account_add'
-
             # validate network and resolve URL safely
             if social_network not in dict(TYPE_CHOICES):
@@
-            try:
+            url_name = f'admin:publisher_{social_network}account_add'
+            try:
                 url = reverse(url_name)

111-115: Catch NoReverseMatch explicitly (not broad Exception).

This keeps error handling precise and avoids masking other issues.

Apply:

-        try:
-            url = reverse(url_name, args=[related_obj_id])
-        except Exception:
+        try:
+            url = reverse(url_name, args=[related_obj_id])
+        except NoReverseMatch:
             self.message_user(request, "Страница редактирования для этой соцсети не найдена.", level=messages.ERROR)
             return super().response_change(request, obj)

142-143: select_related() without fields is a no-op here.

Account has no FK fields to follow (GenericForeignKey isn’t supported by select_related). Remove it.

Apply:

-        accounts = Account.objects.filter(pk__in=pks).select_related()
+        accounts = Account.objects.filter(pk__in=pks)

155-167: Import schedule_task once per request, not per valid form.

Move the import above the loop to avoid repeated imports.

Apply:

-            for acc in accounts:
+            from publisher.scheduler import schedule_task
+            for acc in accounts:
@@
-                if post_form.is_valid():
-                    from publisher.scheduler import schedule_task
+                if post_form.is_valid():
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cc59599 and 704689f.

📒 Files selected for processing (10)
  • publisher/account.py (1 hunks)
  • publisher/middleware.py (1 hunks)
  • publisher/migrations/0001_initial.py (1 hunks)
  • publisher/planned_post.py (1 hunks)
  • publisher/scheduler.py (1 hunks)
  • publisher/socials/telegram.py (1 hunks)
  • publisher/socials/twitter.py (1 hunks)
  • publisher/socials/vk.py (1 hunks)
  • static/js/detect_timezone.js (1 hunks)
  • static/js/planned_post_editor.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • static/js/detect_timezone.js
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-27T14:23:59.310Z
Learnt from: mlv-dev
PR: dtpstat/dtp-stat#132
File: publisher/middleware.py:6-18
Timestamp: 2025-08-27T14:23:59.310Z
Learning: In the dtpstat/dtp-stat project, functionality is sometimes split across multiple PRs, with middleware implementations in one PR and their corresponding settings registrations in separate PRs like #131.

Applied to files:

  • publisher/middleware.py
📚 Learning: 2025-08-27T14:31:34.537Z
Learnt from: mlv-dev
PR: dtpstat/dtp-stat#132
File: publisher/socials/twitter.py:32-65
Timestamp: 2025-08-27T14:31:34.537Z
Learning: In the publisher/socials/twitter.py file, the user prefers case-sensitive HTML regex patterns over case-insensitive ones for parsing tweet content in the clean_publish_data method.

Applied to files:

  • publisher/socials/twitter.py
📚 Learning: 2025-08-26T23:41:27.938Z
Learnt from: mlv-dev
PR: dtpstat/dtp-stat#132
File: posting/migrations/0001_initial.py:71-72
Timestamp: 2025-08-26T23:41:27.938Z
Learning: VkAccount model password field is used to store VK API credentials that are passed as parameters to VK API calls, not for user authentication. These credentials need to be stored in their original form to work with the VK API.

Applied to files:

  • publisher/socials/vk.py
🪛 Ruff (0.12.2)
publisher/planned_post.py

59-59: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


68-68: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

publisher/scheduler.py

30-33: Combine if branches using logical or operator

Combine if branches

(SIM114)

publisher/socials/telegram.py

72-72: SyntaxError: Expected a statement


72-72: SyntaxError: Expected a statement


72-73: SyntaxError: Expected a statement

publisher/socials/twitter.py

96-96: Use a context manager for opening files

(SIM115)


128-131: Use contextlib.suppress(OSError) instead of try-except-pass

Replace with contextlib.suppress(OSError)

(SIM105)

publisher/socials/vk.py

87-90: Use contextlib.suppress(OSError) instead of try-except-pass

Replace with contextlib.suppress(OSError)

(SIM105)

🪛 Biome (2.1.2)
static/js/planned_post_editor.js

[error] 8-8: expected , but instead found function

Remove function

(parse)


[error] 13-13: expected , but instead found function

Remove function

(parse)


[error] 22-22: expected , but instead found function

Remove function

(parse)


[error] 78-78: expected , but instead found function

Remove function

(parse)


[error] 101-101: expected , but instead found const

Remove const

(parse)


[error] 103-103: Illegal return statement outside of a function

(parse)


[error] 113-113: Illegal return statement outside of a function

(parse)

🔇 Additional comments (1)
publisher/middleware.py (1)

10-18: Scoped activation with ZoneInfo fallback looks correct

Good use of timezone.override() and precise handling of ZoneInfoNotFoundError. Implementation is clean and safe.

Comment thread publisher/account.py
Comment thread publisher/migrations/0001_initial.py
Comment thread publisher/migrations/0001_initial.py
Comment thread publisher/planned_post.py
Comment thread publisher/scheduler.py
Comment thread publisher/scheduler.py
Comment thread publisher/socials/telegram.py Outdated
Comment thread publisher/socials/twitter.py
Comment thread static/js/planned_post_editor.js Outdated
Comment thread static/js/planned_post_editor.js
@mlv-dev
Copy link
Copy Markdown
Collaborator Author

mlv-dev commented Aug 28, 2025

Кажется, +- всё

@mlv-dev mlv-dev marked this pull request as ready for review August 28, 2025 23:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant