Redesign ring header into structured 2-row grid, show full now-playing title and thumbnail
Ring header restructured from flat 1fr/auto grid into two semantic rows: - Info row: title + badges left, progress counter right - Controls row: Reverse and Autoplay toggles right-aligned Now-playing row shows full title (no line-clamp truncation) and product thumbnail. JS reads og:image meta tag for current thumbnail during SPA navigation. SPA updates for edit button, download button, file info, comments, and canonical link. Footer and docs updates.
This commit is contained in:
parent
3ade78d512
commit
f780f0dab6
10 changed files with 344 additions and 22 deletions
117
CHANGELOG.rst
117
CHANGELOG.rst
|
|
@ -3,6 +3,123 @@ Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
2026-02-09
|
||||
----------
|
||||
|
||||
DJ Crossfade
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* 7-second audio + visual crossfade starts before current video ends
|
||||
* Volume ramps down on outgoing, ramps up on incoming simultaneously
|
||||
* Opacity crossfade dissolves between videos during transition
|
||||
* Countdown overlay shows during crossfade with Play Now / Cancel controls
|
||||
* User-initiated transitions (click related item, queue) also get visual crossfade
|
||||
* Falls back to standard countdown when preloaded data isn't ready or media types differ
|
||||
|
||||
Footer & Links
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
* Fixed apex domain link to use ``www.makepostsell.com`` in shop footer
|
||||
* Added Source Code (GitLab) and PyPI links to shop footer
|
||||
|
||||
2026-02-08
|
||||
----------
|
||||
|
||||
Watch Mode v2
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
* Continuous playback with crossfade transitions between media
|
||||
* Discovery ring: deterministic content traversal precomputed per shop
|
||||
* Up Next sidebar with compact rows, numbered index, and queue management
|
||||
* Countdown overlay (7s) as frosted-glass bottom bar between items
|
||||
* Queue system: add/remove items, reorder playback
|
||||
* Autoplay toggle switch inline with Up Next heading
|
||||
* Two-phase preload: fast JSON fetch, then buffer media 30s before end
|
||||
* Recently-played tracking: filter items from Up Next for 4 hours
|
||||
* Support for video, audio, and static content (PDFs, images) with auto-advance
|
||||
* Sticky video on desktop scroll (offset below ribbon)
|
||||
* ``watch.js`` — new standalone JS module for all watch mode logic
|
||||
|
||||
Pop-out Media Player
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Draggable pop-out window with prev/next navigation
|
||||
* Keyboard shortcuts and responsive controls
|
||||
* Auto-advance for images/PDFs (60s timer, reset on scroll)
|
||||
* ``player.js`` — new standalone JS module
|
||||
* ``/random`` and ``/tv`` endpoints for media playback
|
||||
|
||||
Lazy Cart Creation
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Anonymous session carts are now in-memory until a product is added
|
||||
* Prevents bots and crawlers from creating empty cart rows in the database
|
||||
* UUID stays stable across requests via session cookie
|
||||
* Transient cart guard in authentication merge flow
|
||||
|
||||
AJAX Comments
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
* Comment form submits via fetch when JS is available
|
||||
* Preserves media playback — no page reload interrupts
|
||||
* Falls back to normal POST + redirect without JS
|
||||
* ``comments.js`` — new standalone JS module
|
||||
|
||||
Email Subscriptions
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Email digest subscriptions with configurable frequency
|
||||
* @mentions in comments notify mentioned users
|
||||
* RSS and Atom feed autodiscovery in ``<head>``
|
||||
* Subscribe link in shop nav bar
|
||||
|
||||
Related Content
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
* Jaccard similarity for related content ranking
|
||||
* Related content thumbnails in sidebar
|
||||
|
||||
Feeds & SEO
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Sitemap generation (``/sitemap.xml``)
|
||||
* RSS (``/rss.xml``) and Atom (``/atom.xml``) feeds
|
||||
* Google site verification meta tag support
|
||||
* Feeds served as ``application/xml`` for browser rendering
|
||||
* Feed links open in new window
|
||||
|
||||
Adyen Integration
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Added Adyen as a payment processor
|
||||
* Per-shop enable/disable toggle
|
||||
* See ``docs/ADYEN.md`` for details
|
||||
|
||||
Stripe Improvements
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Payment tracking and webhook resilience
|
||||
* Per-shop Stripe enable/disable
|
||||
|
||||
UI & Layout
|
||||
~~~~~~~~~~~
|
||||
|
||||
* Sticky ribbon + logo/nav on desktop scroll
|
||||
* Footer pushed to bottom of viewport on short pages
|
||||
* Product media uses viewport-relative sizing (``max-height: 42vh``)
|
||||
* Reduced heading font sizes for tighter layout
|
||||
* Product/content dates toggle (``show_dates`` shop setting)
|
||||
* Inline video playback on thumbnail click (replaced pop-up)
|
||||
|
||||
Infrastructure
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
* ``/version`` endpoint with baked git hash for deploy verification
|
||||
* ``GIT_HASH`` tracked in repo, baked at install time
|
||||
* Removed jQuery — all vanilla JavaScript
|
||||
* Replaced JS toggles with pure CSS ``<details>`` elements
|
||||
* Pinned ``setuptools<81`` for Python 3.12+ compatibility
|
||||
|
||||
2025-12-22 (2:30 PM)
|
||||
--------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -218,6 +218,13 @@ Always use `uuid_str` when you need a string copy of the identifier. Models inhe
|
|||
|
||||
**MOBILE USABILITY**: Never use hover-only interactions (`:hover` to reveal controls, `opacity: 0` with hover reveal, etc.). Mobile/touch devices have no hover state — controls hidden behind hover are invisible and unreachable. All interactive elements (buttons, toggles, links) must be always visible and tappable. Design touch-first, then optionally enhance for desktop hover.
|
||||
|
||||
**SPA + NORMAL MODE**: Watch mode uses SPA navigation (`watch.js`) that swaps content without a full page reload. When adding or modifying links, buttons, forms, or any product-specific content on pages that participate in watch mode (content.j2, product.j2), you MUST ensure:
|
||||
1. **Server-rendered HTML** works for the initial page load (normal mode, no-JS, crawlers)
|
||||
2. **`updatePageContent()` in watch.js** updates the same element during SPA navigation
|
||||
3. **The watch JSON endpoint** (`watch.py`) returns any new data the JS needs
|
||||
|
||||
Elements that must stay in sync: CTA edit button, download button, comment form `product_id`, file type/size, description, title, canonical link, related items. If you add a new product-specific element, add it to all three layers.
|
||||
|
||||
**TESTING INTEGRITY**: NEVER skip, delete, or disable unit tests or integration tests when they break. When tests fail:
|
||||
1. **FIX THE TESTS** - Update them to work with new functionality
|
||||
2. **FIX THE CODE** - If the tests reveal actual defects, fix the underlying issue
|
||||
|
|
|
|||
20
README.rst
20
README.rst
|
|
@ -7,6 +7,26 @@ You can use the SaaS or self-host! Accepts credit cards (Stripe), PayPal, Monero
|
|||
|
||||
Our `blog acts as our user guide <https://blog.makepostsell.com/>`_ & also uses ``make_post_sell``!
|
||||
|
||||
| `Source Code <https://git.unturf.com/engineering/make-post-sell/make_post_sell>`_ | `PyPI <https://pypi.org/project/make-post-sell/>`_ | `SaaS <https://my.makepostsell.com>`_ |
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* **Multi-tenant SaaS** — each shop gets its own subdomain or custom domain
|
||||
* **Payments** — Stripe, PayPal, Adyen, Monero (XMR), Dogecoin (DOGE)
|
||||
* **Watch Mode** — continuous media playback with DJ crossfade transitions, discovery ring, queue management, and autoplay
|
||||
* **Pop-out Player** — draggable media window with keyboard shortcuts, ``/random`` and ``/tv`` endpoints
|
||||
* **Email Subscriptions** — digest emails (immediately, daily, weekly) with @mention notifications
|
||||
* **Feeds** — RSS, Atom, and sitemap generation per shop
|
||||
* **AJAX Comments** — real-time comment posting without page reload, preserves media playback
|
||||
* **Related Content** — Jaccard similarity ranking with thumbnails
|
||||
* **Lazy Carts** — in-memory until a product is added, no empty rows from bots
|
||||
* **CSS Grid Lanes** — optional masonry layout per shop
|
||||
* **Vanilla JS** — no jQuery, no framework dependencies
|
||||
* **Public Domain** — all contributed code is placed in the public domain
|
||||
|
||||
See ``CHANGELOG.rst`` for detailed release history.
|
||||
|
||||
|
||||
Quick Start: Operating a Server with PyPI or Source Code
|
||||
==========================================================
|
||||
|
|
|
|||
|
|
@ -24,6 +24,33 @@ Located in `make_post_sell/static/js/`:
|
|||
- Functions: `previewAjax()`, `sendPreview()`
|
||||
- Uses fetch API for AJAX calls to `/markup-editor-preview`
|
||||
|
||||
### watch.js
|
||||
- Watch mode continuous playback engine
|
||||
- Used by: `product.j2` when shop has `watch_mode_enabled`
|
||||
- Discovery ring with deterministic content traversal per shop
|
||||
- Crossfade transitions between media (3s, 60 steps)
|
||||
- Countdown overlay (7s) between items
|
||||
- Queue management: add, remove, reorder
|
||||
- Two-phase preload: JSON metadata fetch, then media buffering 30s before end
|
||||
- Recently-played filtering via localStorage (4-hour expiry)
|
||||
- Handles video, audio, and static content (PDFs, images)
|
||||
- Autoplay toggle persisted in localStorage
|
||||
- Ring state (position, direction, history) persisted in localStorage
|
||||
|
||||
### comments.js
|
||||
- AJAX comment submission (progressive enhancement)
|
||||
- Intercepts comment form POST, submits via `fetch()` with `X-Requested-With: XMLHttpRequest`
|
||||
- Server returns JSON (HTTP 201) for AJAX requests
|
||||
- Inserts new comment into DOM without page reload (preserves media playback)
|
||||
- Falls back to normal POST + redirect without JS or on error
|
||||
|
||||
### player.js
|
||||
- Pop-out media player with draggable window
|
||||
- Prev/next navigation and keyboard shortcuts
|
||||
- Auto-advance for images/PDFs (60s timer)
|
||||
- Preloads adjacent media for instant switching
|
||||
- Used by: `/random` and `/tv` endpoints
|
||||
|
||||
## Inline JavaScript by Template
|
||||
|
||||
### base.j2
|
||||
|
|
@ -84,6 +111,19 @@ Located in `make_post_sell/static/js/`:
|
|||
- Google Analytics (gtag.js)
|
||||
- Only loaded if shop has configured analytics
|
||||
|
||||
### product.j2
|
||||
**Purpose:** Inline video playback
|
||||
|
||||
- `playInline()` replaces thumbnail container with `<video>` element on click
|
||||
- Used for preview playback on product pages without watch mode
|
||||
- Loads `watch.js` when shop has `watch_mode_enabled`
|
||||
|
||||
### snippets/comments.j2
|
||||
**Purpose:** Comment form
|
||||
|
||||
- Loads `comments.js` async for AJAX submission
|
||||
- Uses `js-only` class pattern for elements requiring JS
|
||||
|
||||
### snippets/optional-javascript.j2
|
||||
**Purpose:** Optional JS loading
|
||||
|
||||
|
|
@ -111,8 +151,6 @@ Pages work without JavaScript where possible:
|
|||
- Payment provider toggles use `<details>` (pure CSS)
|
||||
- Forms submit normally without JS
|
||||
- JS enhances UX (copy buttons, QR codes, live previews)
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- Convert remaining checkbox toggles to `<details>` elements
|
||||
- Consider removing Google Analytics option (privacy)
|
||||
- Comment forms POST normally without JS; AJAX submission preserves playback with JS
|
||||
- Watch mode elements use `js-only` class (hidden via `<noscript>` stylesheet)
|
||||
- Video thumbnails link directly to media without JS; inline playback with JS
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
da46586
|
||||
3ade78d
|
||||
|
|
@ -2681,11 +2681,29 @@ textarea {
|
|||
|
||||
.related-content-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ring-header-info {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto 1fr;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ring-header-info .ring-progress {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.ring-header-controls {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 16px;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.related-content-header h3 {
|
||||
margin: 0;
|
||||
font-size: 0.85em;
|
||||
|
|
@ -2773,6 +2791,10 @@ textarea {
|
|||
.related-content-now-playing {
|
||||
font-weight: bold;
|
||||
}
|
||||
.related-content-now-playing .related-content-title {
|
||||
-webkit-line-clamp: unset;
|
||||
overflow: visible;
|
||||
}
|
||||
.related-content-row-prev {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -513,6 +513,67 @@
|
|||
descSection.innerHTML = data.description_html;
|
||||
}
|
||||
|
||||
// Update CTA edit button href for mod users
|
||||
if (isMod && data.product_id) {
|
||||
var editBtn = document.querySelector('.call-to-action .product-edit-button');
|
||||
if (editBtn) {
|
||||
var prefix = data.is_sellable ? '/p/' : '/c/';
|
||||
editBtn.href = prefix + data.product_id + '/edit';
|
||||
}
|
||||
}
|
||||
|
||||
// Update download button
|
||||
var dlBtn = document.querySelector('.product-download-button');
|
||||
var dlWell = dlBtn ? dlBtn.closest('.well') : null;
|
||||
if (data.has_product_file && data.download_url) {
|
||||
if (dlBtn) {
|
||||
dlBtn.href = data.download_url;
|
||||
} else if (dlWell) {
|
||||
dlWell.innerHTML = '<a href="' + data.download_url
|
||||
+ '" class="product-download-button mps-button" target="_blank" download>⭳ Download</a>';
|
||||
}
|
||||
if (dlWell) dlWell.style.display = '';
|
||||
} else {
|
||||
if (dlWell) dlWell.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update file type/size info
|
||||
var descDiv = document.querySelector('.product-description');
|
||||
if (descDiv) {
|
||||
var fileInfoB = descDiv.querySelectorAll('b');
|
||||
for (var i = 0; i < fileInfoB.length; i++) {
|
||||
if (fileInfoB[i].textContent === 'File Type') {
|
||||
// The text node after <b>File Type</b><br/> holds "content/type size"
|
||||
var sibling = fileInfoB[i].nextSibling;
|
||||
while (sibling && sibling.nodeName === 'BR') sibling = sibling.nextSibling;
|
||||
if (sibling && sibling.nodeType === Node.TEXT_NODE) {
|
||||
if (data.has_product_file && data.file_type) {
|
||||
sibling.textContent = data.file_type + ' ' + (data.file_size || '');
|
||||
} else {
|
||||
sibling.textContent = '';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update comment form product_id and clear stale comments
|
||||
var commentForms = document.querySelectorAll('input[name="product_id"]');
|
||||
for (var j = 0; j < commentForms.length; j++) {
|
||||
commentForms[j].value = data.product_id;
|
||||
}
|
||||
var commentsList = document.getElementById('comments-list');
|
||||
if (commentsList) commentsList.innerHTML = '';
|
||||
var commentsHeading = document.querySelector('.comments-section h3');
|
||||
if (commentsHeading) commentsHeading.textContent = 'Comments & Reviews';
|
||||
|
||||
// Update canonical link
|
||||
var canonicalLink = document.querySelector('link[rel="canonical"]');
|
||||
if (canonicalLink && data.canonical_url) {
|
||||
canonicalLink.href = window.location.origin + data.canonical_url;
|
||||
}
|
||||
|
||||
updateRelated(data.related);
|
||||
|
||||
if (data.canonical_url) {
|
||||
|
|
@ -539,20 +600,24 @@
|
|||
? '<span id="ring-loops-badge" class="ring-loops-badge">+' + ringLoops + '</span>'
|
||||
: '<span id="ring-loops-badge" class="ring-loops-badge" style="display:none"></span>';
|
||||
var headerHtml = '<div class="related-content-header">'
|
||||
+ '<div class="ring-header-info">'
|
||||
+ '<h3>Up Next</h3>'
|
||||
+ modBadge
|
||||
+ '<span id="ring-progress" class="ring-progress"></span>'
|
||||
+ loopsBadge
|
||||
+ '<span id="ring-progress" class="ring-progress"></span>'
|
||||
+ '</div>'
|
||||
+ '<div class="ring-header-controls">'
|
||||
+ '<label class="watch-direction-label js-only">'
|
||||
+ '<span class="direction-label-text">Reverse</span>'
|
||||
+ '<input type="checkbox" id="watch-direction-toggle"' + directionChecked + ' />'
|
||||
+ '<span class="autoplay-slider"></span>'
|
||||
+ '</label>'
|
||||
+ '<label class="watch-autoplay-label js-only">'
|
||||
+ '<span class="autoplay-label-text">Autoplay</span>'
|
||||
+ '<input type="checkbox" id="watch-autoplay-toggle"' + autoplayChecked + ' />'
|
||||
+ '<span class="autoplay-slider"></span>'
|
||||
+ '</label>'
|
||||
+ '<label class="watch-direction-label js-only">'
|
||||
+ '<span class="direction-label-text">Reverse</span>'
|
||||
+ '<input type="checkbox" id="watch-direction-toggle"' + directionChecked + ' />'
|
||||
+ '<span class="autoplay-slider"></span>'
|
||||
+ '</label></div>';
|
||||
+ '</div></div>';
|
||||
|
||||
if (!related || related.length === 0) {
|
||||
container.innerHTML = headerHtml + '<p>No more items</p>';
|
||||
|
|
@ -573,9 +638,16 @@
|
|||
// Insert "now playing" row before the first forward item
|
||||
if (offset > 0 && !insertedCurrent) {
|
||||
insertedCurrent = true;
|
||||
var ogImg = document.querySelector('meta[property="og:image"]');
|
||||
var currentThumb = ogImg ? ogImg.getAttribute('content') : '';
|
||||
html += '<div class="related-content-row related-content-row-current">';
|
||||
html += '<span class="related-content-index">▶</span>';
|
||||
html += '<span class="related-content-item related-content-now-playing">';
|
||||
if (currentThumb) {
|
||||
html += '<img src="' + currentThumb + '" />';
|
||||
} else {
|
||||
html += '<span class="related-content-no-thumb"></span>';
|
||||
}
|
||||
html += '<span class="related-content-title">' + escapeHtml(currentTitle) + '</span>';
|
||||
html += '</span>';
|
||||
html += '</div>';
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@
|
|||
<span class="mps-footer-group-title">Account</span>
|
||||
<nav class="mps-footer-links">
|
||||
<a href="/u/settings">My Account</a>
|
||||
<a href="https://makepostsell.com" target="_blank">Powered by makepostsell.com</a>
|
||||
<a href="https://www.makepostsell.com" target="_blank">Powered by makepostsell.com</a>
|
||||
<a href="https://git.unturf.com/engineering/make-post-sell/make_post_sell" target="_blank">Source Code</a>
|
||||
<a href="https://pypi.org/project/make-post-sell/" target="_blank">PyPi</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
{% if related_products %}
|
||||
<div class="related-content">
|
||||
<div class="related-content-header">
|
||||
<h3>Up Next</h3>
|
||||
<div class="ring-header-info">
|
||||
<h3>Up Next</h3>
|
||||
{% if request.shop.watch_mode_enabled %}
|
||||
<span id="ring-progress" class="ring-progress"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if request.shop.watch_mode_enabled %}
|
||||
<span id="ring-progress" class="ring-progress"></span>
|
||||
<label class="watch-autoplay-label js-only">
|
||||
<span class="autoplay-label-text">Autoplay</span>
|
||||
<input type="checkbox" id="watch-autoplay-toggle" checked />
|
||||
<span class="autoplay-slider"></span>
|
||||
</label>
|
||||
<div class="ring-header-controls">
|
||||
<label class="watch-direction-label js-only">
|
||||
<span class="direction-label-text">Reverse</span>
|
||||
<input type="checkbox" id="watch-direction-toggle" />
|
||||
<span class="autoplay-slider"></span>
|
||||
</label>
|
||||
<label class="watch-autoplay-label js-only">
|
||||
<span class="autoplay-label-text">Autoplay</span>
|
||||
<input type="checkbox" id="watch-autoplay-toggle" checked />
|
||||
<span class="autoplay-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for item in related_products %}
|
||||
|
|
@ -23,6 +34,11 @@
|
|||
<div class="related-content-row related-content-row-current">
|
||||
<span class="related-content-index">▶</span>
|
||||
<span class="related-content-item related-content-now-playing">
|
||||
{% if "thumbnail1" in product.extensions %}
|
||||
<img src="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" />
|
||||
{% else %}
|
||||
<span class="related-content-no-thumb"></span>
|
||||
{% endif %}
|
||||
<span class="related-content-title">{{ product.title }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -116,6 +116,30 @@ def watch_json(request):
|
|||
"offset": offset,
|
||||
})
|
||||
|
||||
# Generate download URL (attachment disposition) for the product file
|
||||
download_url = None
|
||||
has_product_file = product.has_product_file
|
||||
file_type_str = ""
|
||||
file_size_str = ""
|
||||
if has_product_file:
|
||||
dl_content_disposition = product.get_content_disposition(file_key)
|
||||
dl_content_type = product.get_content_type(file_key)
|
||||
dl_params = {"Bucket": bucket_name, "Key": s3_key}
|
||||
if dl_content_disposition:
|
||||
# Force attachment for download
|
||||
dl_params["ResponseContentDisposition"] = dl_content_disposition.replace(
|
||||
"inline;", "attachment;"
|
||||
)
|
||||
if dl_content_type:
|
||||
dl_params["ResponseContentType"] = dl_content_type
|
||||
file_type_str = dl_content_type
|
||||
file_size_str = product.human_file_bytes(file_key)
|
||||
download_url = request.secure_uploads_client.generate_presigned_url(
|
||||
ClientMethod="get_object",
|
||||
Params=dl_params,
|
||||
ExpiresIn=900,
|
||||
)
|
||||
|
||||
# Check if current user is a shop editor (mod)
|
||||
is_mod = False
|
||||
if request.user and request.user.authenticated:
|
||||
|
|
@ -130,6 +154,10 @@ def watch_json(request):
|
|||
"description_html": product.description_html,
|
||||
"canonical_url": canonical_url,
|
||||
"is_sellable": product.is_sellable,
|
||||
"has_product_file": has_product_file,
|
||||
"download_url": download_url,
|
||||
"file_type": file_type_str,
|
||||
"file_size": file_size_str,
|
||||
"related": related_data,
|
||||
"ring": ring,
|
||||
"is_mod": is_mod,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue