65 Commits
Author SHA1 Message Date
Jarek Radosz bc68d6e230 DEV: Fix migration timestamp (#254) 2025-07-22 11:20:13 +01:00
Jarek Radosz b521d3e909 DEV: Mention the core move in readme (#253) 2025-07-22 11:00:07 +01:00
Jarek Radosz e9c3c335f2 DEV: Disable the plugin by default (#252)
…and preserve the current setting on existing sites
2025-07-22 11:50:37 +02:00
Jarek Radosz 0171ad8d3a Revert "DEV: Deprecate the plugin (#250)" (#251)
This reverts commit 7d34144615.
2025-07-15 10:25:25 +01:00
Jarek Radosz 7d34144615 DEV: Deprecate the plugin (#250) 2025-07-14 10:16:57 +01:00
Sérgio Saquetim 8a3263d050 DEV: Add compatibility with the Glimmer Post Stream (#247)
This commit updates the ad plugin to support the Glimmer Post Stream and refactors ad rendering logic for improved compatibility and maintainability.

Key changes:

- Introduce glimmer_post_stream_mode compatibility across all ad types and test suites.
- Replace .widget-connector with .ad-connector for ad placement after posts.
- Migrate initialize-ad-plugin.js to initialize-ad-plugin.gjs to leverage Glimmer and API improvements.
- Use withSilencedDeprecations to handle widget overrides for post ads.
- Update compatibility constraints in .discourse-compatibility.
- Refactor and expand acceptance tests for AdSense, DFP, and House Ads to run with both Glimmer and legacy post stream modes.
2025-07-08 16:28:54 -03:00
David Taylor e51aad3bfd DEV: Resolve @on import deprecation (#248) 2025-07-03 22:58:25 +01:00
dependabot[bot] fcd734079b Bump rack from 3.1.12 to 3.1.16 (#246)
Bumps [rack](https://github.com/rack/rack) from 3.1.12 to 3.1.16.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v3.1.12...v3.1.16)

---
updated-dependencies:
- dependency-name: rack
  dependency-version: 3.1.16
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 17:45:02 +02:00
Jarek Radosz a98a223a35 DEV: Update linting config and run gjs-codemod 2025-06-05 11:34:48 +01:00
Loïc Guitaut 123dc9e6d8 DEV: Add system spec to check core features are working fine (#242) 2025-04-09 14:19:55 +02:00
Jarek Radosz 2f3916c761 DEV: Update linting (#241) 2025-03-17 10:55:10 +00:00
Jarek Radosz 6a5bfcc782 DEV: Update linting (#240) 2025-03-13 13:25:35 +00:00
dependabot[bot] 32ccafecde Bump rack from 3.0.11 to 3.0.12 (#237)
Bumps [rack](https://github.com/rack/rack) from 3.0.11 to 3.0.12.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/commits/v3.0.12)

---
updated-dependencies:
- dependency-name: rack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 23:25:11 +00:00
David Taylor 1f70537968 DEV: Bump dependencies and fix linting (#236) 2025-02-06 17:42:21 +01:00
David Taylor 6aab733bb9 DEV: Pin version for Discourse <3.5.0.beta1-dev (#235) 2025-02-05 19:57:37 +01:00
Kelv 6ceba62e98 DEV: Update more deprecated Font Awesome icon names (#233) 2024-12-06 20:44:52 +01:00
David Taylor 44b17146c0 FIX: HouseAdsChooser onChange handling (#234)
Having an action named the same as an argument is no longer possible in a classic component. Move the logic to the parent instead.

Followup to 7685ebf396
2024-12-06 16:02:29 +00:00
Kelv 00298bf01a DEV: Update deprecated Font Awesome icon names (#232) 2024-12-04 02:38:01 +01:00
David Taylor 7685ebf396 DEV: Update to native class syntax (#230) 2024-11-29 10:11:21 +00:00
David Taylor 4506b0b837 DEV: Update linting (#229) 2024-11-28 15:49:06 +01:00
David Taylor 0400165798 DEV: Update linting (#228) 2024-11-20 18:50:41 +01:00
David Taylor 119d331759 DEV: Update linting (#227) 2024-11-20 15:43:19 +01:00
dependabot[bot] e28f97a0f4 Bump cross-spawn from 7.0.3 to 7.0.6 (#226)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-19 10:37:59 +00:00
David Taylor 1d3baa5519 DEV: Update eslint config (#225) 2024-11-19 11:18:26 +01:00
Discourse Translator Bot 21de3a2b10 Update translations (#224) 2024-10-29 15:31:18 +01:00
dependabot[bot] c91c456187 Bump rexml from 3.3.6 to 3.3.9 (#223)
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.6 to 3.3.9.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.6...v3.3.9)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 20:43:51 +01:00
Kelv e42b90507e DEV: Switch to use pnpm (#222) 2024-10-14 13:37:11 +02:00
dependabot[bot] 38354f2a2f Bump micromatch from 4.0.5 to 4.0.8 (#221)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5 to 4.0.8.
- [Release notes](https://github.com/micromatch/micromatch/releases)
- [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8)

---
updated-dependencies:
- dependency-name: micromatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-01 12:46:36 +02:00
dependabot[bot] fc109c75c4 Bump rexml from 3.3.3 to 3.3.6 (#219)
Bumps [rexml](https://github.com/ruby/rexml) from 3.3.3 to 3.3.6.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.3...v3.3.6)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-01 12:31:59 +02:00
Jarek Radosz 35cc34ab17 DEV: Update ace-editor usage (#220)
AceEditor is now a glimmer component (see: https://github.com/discourse/discourse/pull/28492) and it follows the "data down, actions up" pattern.
2024-08-27 13:53:15 +02:00
Discourse Translator Bot 95993e7d93 Update translations (#218) 2024-08-13 16:30:54 +02:00
dependabot[bot] 10093e0a9f Bump rexml from 3.2.8 to 3.3.3 (#217)
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.8 to 3.3.3.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.8...v3.3.3)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 13:16:47 +02:00
Natalie Tay 02f602236a DEV: Pin plugin for Discourse < 3.4.0.beta1-dev (#216) 2024-08-02 17:03:18 +08:00
Discourse Translator Bot da38a4c053 Update translations (#215) 2024-07-31 00:13:50 +02:00
Arpit Jalan ec53a0d6b8 FIX: add a fix for cases where ad record is not found (#214) 2024-07-05 13:00:18 +05:30
Arpit Jalan 9b72130fdf FIX: show non-restricted ads instead of not showing ads at all (#213)
In some cases where there were category restricted house ads we were not
showing ads on reload. This commit filter out all the ads that should
not be shown on current page, leaving only allowed ads. So now we'll
show ads on every reload in all the cases.

Internal ticket: t130920
2024-06-27 22:13:54 +05:30
Discourse Translator Bot a38fbd0935 Update translations (#212) 2024-06-25 15:39:10 +02:00
dependabot[bot] ea9a0def55 Bump braces from 3.0.2 to 3.0.3 (#211)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-16 22:30:23 +02:00
Discourse Translator Bot 81843e8c98 Update translations (#210) 2024-06-04 21:43:55 +02:00
Discourse Translator Bot 114639d2e7 Update translations (#209) 2024-05-31 12:26:24 +02:00
Loïc Guitaut 5f1e07c556 DEV: Update rubocop-discourse to version 3.8.0 (#208) 2024-05-28 11:11:14 +02:00
dependabot[bot] f5c1b6da59 Bump rexml from 3.2.6 to 3.2.8 (#207)
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.6 to 3.2.8.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.6...v3.2.8)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-16 23:54:19 +02:00
Discourse Translator Bot 6cd52a4966 Update translations (#206) 2024-04-16 17:37:40 +02:00
Isaac Janzen 554f03f3da FEATURE: Add group and category restrictions to house ads (#205)
# Description

This PR adds the ability to apply **group** and **category** restrictions to a **house ad**.

# What is included
- In order to get the group and category selectors to work within `admin/assets/javascripts/discourse/controllers/admin-plugins-house-ads-show.js` I needed to modernize the file. 
- I dropped the `bufferedProperty` implementation in favor of a vanilla ember approach
- I added `category_ids` and `group_ids` to our house ads model
- I added tests for group / category restrictions
- I added a preview button to display the house ad
- `/site.json` would return a object called `house_creatives` and a list of key value pairs that matched the ad name with the html, like so:
```js
{ AD_KEY: ad.html }
```
I need access to the category ids on the client to conditionally render the house ads so the new format will be: 
```js
{ AD_KEY: { html: ad.html, category_ids: ad.category_ids } }
```

# Screenshots
<img width="658" alt="Screenshot 2024-04-08 at 2 39 22 PM" src="https://github.com/discourse/discourse-adplugin/assets/50783505/b44b386d-65a1-4a2a-a487-d735b13357dd">

# Preview Video

https://github.com/discourse/discourse-adplugin/assets/50783505/6d0d8253-afef-4e15-b6fc-c6f696efd169
2024-04-09 11:54:11 -06:00
Loïc Guitaut c4227de1b5 DEV: Fix new Rubocop offenses 2024-03-05 18:12:49 +01:00
Selase Krakani 84d9b1a6da DEV: Pin plugin for Discourse < 3.3.0.beta1-dev (#203) 2024-02-29 13:48:36 -07:00
Discourse Translator Bot 062efc8a54 Update translations (#202) 2024-02-23 09:42:33 +01:00
David Taylor 32f18893ec FEATURE: Improve DFP / Ad-manager Content-Security-Policy compat (#201)
Switching to cross-domain iframes is recommended by google here: https://developers.google.com/publisher-tag/guides/content-security-policy
2024-02-19 12:35:22 +00:00
Blake Erickson b0c95114ea FIX: Calculate no ads for groups server side (#200)
If the selected group to not display ads to had its visibility set to
not be visible then this setting wouldn't work correctly because that
group wouldn't be available client side. The change moves that group
check to be server side so that we can correctly see all the groups that
should not see ads.
2024-02-15 14:52:15 -07:00
David Taylor 1677f7ae39 DEV: Pass post instead of transformedPost to post-bottom-ad (#199)
Changes in 0948d6b19e caused the post-bottom-ad component to start receiving the widget-ised 'transformedPost' instead of the original `post` model. In most cases this difference didn't matter, but it did cause noticable issues with `@model.category` and `@model.topic`.

This commit also deletes the unused `post-bottom/discourse-adplugin.hbs` connector. In the past, the adplugin itself was defining the post-bottom outlet. But now, we use RenderGlimmer to load the `post-bottom-ad` directly.
2024-02-14 13:59:36 +00:00
Discourse Translator Bot 751916b8e3 Update translations (#198) 2024-02-13 16:11:16 +01:00
Blake Erickson 9581367239 FIX: Add exclude groups for each ad platforms (#197)
With the new group system for displaying ads we no longer can check if a
user belongs to a trust level group lower than specified. The other
problem is that ALL users including staff and higher trust levels all
belong to trust level 0. So without this fix if we say that an ad should
be visible to trust level 0 users then it will be shown to all users.

This fix adds a new default setting for each ad platform for excluding
trust level 3, 4, and staff users from being shown ads.

- Make display_groups hidden (they will be removed in a later commit)
- Switch to using only exclude_groups instead of display groups and
  exclude groups
- rename showToDisplayGroups to showXAds for each provider
2024-02-08 19:42:40 -07:00
Blake Erickson 6b5412826f DEV: Update migration to match new site settings (#196)
Forgot to update these values in the previous PR.

Follow up to: https://github.com/discourse/discourse-adplugin/pull/195
2024-01-31 11:17:56 -07:00
Blake EricksonandMartin Brennan 6f8ff34ffa DEV: Convert TL settings to groups (#195)
* DEV: Convert TL settings to groups

This change converts the TL site settings in this plugin to use groups
instead.

See: https://meta.discourse.org/t/283408

Co-authored-by: Martin Brennan <[email protected]>
2024-01-31 11:04:24 -07:00
Discourse Translator Bot 19ac017dd4 Update translations (#194) 2024-01-16 14:54:13 +01:00
Jarek Radosz 429c9644de DEV: Update linting (#191) 2024-01-15 11:24:24 +00:00
David Taylor de7d8ce497 DEV: Resolve more link-to deprecations for ember upgrade (#193) 2024-01-09 20:07:04 +00:00
David Taylor 51af568c92 DEV: Resolve link-to deprecation for Ember upgrade (#192) 2023-12-13 14:59:32 +00:00
Martin BrennanandAlexDev 1d39cd8c80 Update plugin.rb metadata (#183)
Co-authored-by: AlexDev <[email protected]>
2023-11-08 11:16:53 -08:00
David Taylor 36f4ebc64b DEV: Abort placing adsense if Ember component is destroyed (#190)
This should avoid surprising error messages being printed to the console
2023-11-08 10:44:39 +11:00
David Taylor 789be84744 DEV: Log errors from adsense (#189) 2023-11-07 22:44:02 +00:00
David Taylor c88bb59d81 FIX: Fully re-render ads when navigating between pages (#188)
In the past, the ad plugin relied on two side-effects to achieve this behaviour:

1. Components being fully destroyed/rendered when navigating between pages. This stopped working when Discourse core moved to the more efficient 'loading slider' UI

2. The `listLoading` argument. This was an implementation detail of the old discovery routing infrastructure. Core recently overhauled this and removed the `listLoading` argument, because loading is now handled properly by the Ember router.

Instead of these two properties, we can use the `currentRoute` property of Ember's router service to trigger changes when navigating between pages. A common `{{#each` trick is used to fully destroy/re-render components even if the ad network is unchanged.
2023-11-07 21:12:30 +00:00
Discourse Translator Bot 5ad841de0b Update translations (#184) 2023-11-07 21:31:35 +01:00
David Taylor 0948d6b19e DEV: Refactor initializer to use RenderGlimmer and registerWidgetShim 2023-11-07 18:37:39 +00:00
David Taylor 3828578d5d DEV: Restructure file locations
- Colocate components / connectors
- Move admin UI files to `admin/assets` so they are only served to admins
2023-11-07 18:37:39 +00:00
162 changed files with 7024 additions and 4728 deletions
+6 -1
View File
@@ -1,2 +1,7 @@
< 3.5.0.beta8-dev: e51aad3bfd41cf2cb9557e025e833122a9f6c892
< 3.5.0.beta5-dev: bd9af0b9a577bfd2c01cc8b81a782a4e8b81b9c5
< 3.5.0.beta1-dev: 6ceba62e985223c269782c4bc9a1bdc4a5adba28
< 3.4.0.beta2-dev: 7685ebf396c93e8accc5a76a81fcec4384a73fa3
< 3.4.0.beta1-dev: da38a4c053b68d7076db4441cfe86b473979945d
< 3.3.0.beta1-dev: 062efc8a547c19474eaeefae464837d63a5e5fb3
3.1.999: 30c49eb50d6d16fcf82bace3da0e1972b7b220c0 3.1.999: 30c49eb50d6d16fcf82bace3da0e1972b7b220c0
-1
View File
@@ -1 +0,0 @@
module.exports = require("@discourse/lint-configs/eslint");
+2
View File
@@ -0,0 +1,2 @@
engine-strict = true
auto-install-peers = false
+80 -32
View File
@@ -1,54 +1,102 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
ast (2.4.2) activesupport (8.0.2)
json (2.6.3) base64
language_server-protocol (3.17.0.3) benchmark (>= 0.3)
parallel (1.23.0) bigdecimal
parser (3.2.2.4) concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
ast (2.4.3)
base64 (0.3.0)
benchmark (0.4.1)
bigdecimal (3.2.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
drb (2.2.3)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
json (2.12.2)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
minitest (5.25.5)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
prettier_print (1.2.1) prettier_print (1.2.1)
racc (1.7.3) prism (1.4.0)
racc (1.8.1)
rack (3.1.16)
rainbow (3.1.1) rainbow (3.1.1)
regexp_parser (2.8.2) regexp_parser (2.10.0)
rexml (3.2.6) rubocop (1.75.8)
rubocop (1.57.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.2.4) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.44.0, < 2.0)
rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.30.0) rubocop-ast (1.44.1)
parser (>= 3.2.1.0) parser (>= 3.3.7.2)
rubocop-capybara (2.19.0) prism (~> 1.4)
rubocop (~> 1.41) rubocop-capybara (2.22.1)
rubocop-discourse (3.4.0) lint_roller (~> 1.1)
rubocop (>= 1.1.0) rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (>= 2.0.0) rubocop-discourse (3.12.1)
rubocop-factory_bot (2.24.0) activesupport (>= 6.1)
rubocop (~> 1.33) lint_roller (>= 1.1.0)
rubocop-rspec (2.25.0) rubocop (>= 1.73.2)
rubocop (~> 1.40) rubocop-capybara (>= 2.22.0)
rubocop-capybara (~> 2.17) rubocop-factory_bot (>= 2.27.0)
rubocop-factory_bot (~> 2.22) rubocop-rails (>= 2.30.3)
rubocop-rspec (>= 3.0.1)
rubocop-rspec_rails (>= 2.31.0)
rubocop-factory_bot (2.27.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.32.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.6.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
securerandom (0.4.1)
syntax_tree (6.2.0) syntax_tree (6.2.0)
prettier_print (>= 1.2.0) prettier_print (>= 1.2.0)
unicode-display_width (2.5.0) tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
PLATFORMS PLATFORMS
arm64-darwin-21 ruby
x86_64-linux
DEPENDENCIES DEPENDENCIES
rubocop-discourse rubocop-discourse
syntax_tree syntax_tree
BUNDLED WITH BUNDLED WITH
2.4.21 2.6.9
+3
View File
@@ -1,5 +1,8 @@
# Official Discourse Advertising Plugin # Official Discourse Advertising Plugin
> [!IMPORTANT]
> This plugin has now been bundled into Discourse core. See: https://meta.discourse.org/t/bundling-more-popular-plugins-with-discourse-core/373574
Official Plugin Topic & Documentation: https://meta.discourse.org/t/official-advertising-ad-plugin-for-discourse/33734 Official Plugin Topic & Documentation: https://meta.discourse.org/t/official-advertising-ad-plugin-for-discourse/33734
This is the official Discourse advertising plugin. It allows advertisements to be served by supported advertising platforms for users with a Discourse forum. This is the official Discourse advertising plugin. It allows advertisements to be served by supported advertising platforms for users with a Discourse forum.
@@ -0,0 +1,7 @@
import CategorySelector from "select-kit/components/category-selector";
export default class HouseAdsCategorySelector extends CategorySelector {
get value() {
return this.selectedCategories.map((c) => c.id);
}
}
@@ -0,0 +1,33 @@
import { computed } from "@ember/object";
import { classNames } from "@ember-decorators/component";
import { makeArray } from "discourse/lib/helpers";
import MultiSelectComponent from "select-kit/components/multi-select";
@classNames("house-ads-chooser")
export default class HouseAdsChooser extends MultiSelectComponent {
filterable = true;
filterPlaceholder = "admin.adplugin.house_ads.filter_placeholder";
tokenSeparator = "|";
allowCreate = false;
allowAny = false;
settingValue = "";
valueAttribute = null;
nameProperty = null;
@computed("settingValue")
get value() {
return this.settingValue
.toString()
.split(this.tokenSeparator)
.filter(Boolean);
}
computeValues() {
return this.settingValue.split(this.tokenSeparator).filter(Boolean);
}
@computed("choices")
get content() {
return makeArray(this.choices);
}
}
@@ -0,0 +1,34 @@
import { action } from "@ember/object";
import { mapBy } from "@ember/object/computed";
import { classNames } from "@ember-decorators/component";
import DButton from "discourse/components/d-button";
import { makeArray } from "discourse/lib/helpers";
import HouseAdsChooser from "./house-ads-chooser";
import HouseAdsSetting from "./house-ads-setting";
@classNames("house-ads-setting house-ads-list-setting")
export default class HouseAdsListSetting extends HouseAdsSetting {
@mapBy("allAds", "name") adNames;
@action
changeAdValue(value) {
const settingValue = makeArray(value).join("|");
this.set("adValue", settingValue);
}
<template>
<label for={{this.name}}>{{this.title}}</label>
<HouseAdsChooser
@settingValue={{this.adValue}}
@choices={{this.adNames}}
@onChange={{this.changeAdValue}}
/>
<div class="setting-controls">
{{#if this.changed}}
<DButton class="ok" @action={{action "save"}} @icon="check" />
<DButton class="cancel" @action={{action "cancel"}} @icon="xmark" />
{{/if}}
</div>
<p class="help">{{this.help}}</p>
</template>
}
@@ -0,0 +1,72 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { classNames } from "@ember-decorators/component";
import DButton from "discourse/components/d-button";
import TextField from "discourse/components/text-field";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n as computedI18n, propertyNotEqual } from "discourse/lib/computed";
import { i18n } from "discourse-i18n";
@classNames("house-ads-setting")
export default class HouseAdsSetting extends Component {
adValue = "";
saving = false;
savingStatus = "";
@computedI18n("name", "admin.adplugin.house_ads.%@.title") title;
@computedI18n("name", "admin.adplugin.house_ads.%@.description") help;
@propertyNotEqual("adValue", "value") changed;
init() {
super.init(...arguments);
this.set("adValue", this.get("value"));
}
@action
save() {
if (!this.get("saving")) {
this.setProperties({
saving: true,
savingStatus: i18n("saving"),
});
ajax(`/admin/plugins/pluginad/house_settings/${this.get("name")}.json`, {
type: "PUT",
data: { value: this.get("adValue") },
})
.then(() => {
const adSettings = this.get("adSettings");
adSettings.set(this.get("name"), this.get("adValue"));
this.setProperties({
value: this.get("adValue"),
savingStatus: i18n("saved"),
});
})
.catch(popupAjaxError)
.finally(() => {
this.setProperties({
saving: false,
savingStatus: "",
});
});
}
}
@action
cancel() {
this.set("adValue", this.get("value"));
}
<template>
<label for={{this.name}}>{{this.title}}</label>
<TextField @value={{this.adValue}} @classNames="house-ads-text-input" />
<div class="setting-controls">
{{#if this.changed}}
<DButton class="ok" @action={{action "save"}} @icon="check" />
<DButton class="cancel" @action={{action "cancel"}} @icon="xmark" />
{{/if}}
</div>
<p class="help">{{this.help}}</p>
</template>
}
@@ -0,0 +1,18 @@
import { htmlSafe } from "@ember/template";
import DModal from "discourse/components/d-modal";
import { i18n } from "discourse-i18n";
const Preview = <template>
<DModal
@closeModal={{@closeModal}}
@title={{i18n "admin.adplugin.house_ads.preview"}}
>
<:body>
<div class="house-ad-preview">
{{htmlSafe @model.html}}
</div>
</:body>
</DModal>
</template>;
export default Preview;
@@ -0,0 +1,9 @@
import Controller, { inject as controller } from "@ember/controller";
import { alias } from "@ember/object/computed";
export default class AdminPluginsHouseAdsIndexController extends Controller {
@controller("adminPlugins.houseAds") adminPluginsHouseAds;
@alias("adminPluginsHouseAds.model") houseAds;
@alias("adminPluginsHouseAds.houseAdsSettings") adSettings;
}
@@ -0,0 +1,156 @@
import { tracked } from "@glimmer/tracking";
import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { action } from "@ember/object";
import { service } from "@ember/service";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import { observes } from "@ember-decorators/object";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Category from "discourse/models/category";
import { i18n } from "discourse-i18n";
import Preview from "../components/modal/preview";
export default class adminPluginsHouseAdsShow extends Controller {
@service router;
@service modal;
@controller("adminPlugins.houseAds") houseAdsController;
@tracked selectedCategories = [];
@tracked selectedGroups = [];
@tracked saving = false;
@tracked savingStatus = "";
@tracked buffered;
@observes("model")
modelChanged() {
this.buffered = new TrackedObject({ ...this.model });
this.selectedCategories = this.model.categories || [];
this.selectedGroups = this.model.group_ids || [];
}
get disabledSave() {
for (const key in this.buffered) {
// we don't want to compare the categories array
if (key !== "categories" && this.buffered[key] !== this.model[key]) {
return false;
}
}
return true;
}
@action
async save() {
if (!this.saving) {
this.saving = true;
this.savingStatus = i18n("saving");
const data = {};
const newRecord = !this.buffered.id;
if (!newRecord) {
data.id = this.buffered.id;
}
data.name = this.buffered.name;
data.html = this.buffered.html;
data.visible_to_logged_in_users =
this.buffered.visible_to_logged_in_users;
data.visible_to_anons = this.buffered.visible_to_anons;
data.category_ids = this.buffered.category_ids;
data.group_ids = this.buffered.group_ids;
try {
const ajaxData = await ajax(
newRecord
? `/admin/plugins/pluginad/house_creatives`
: `/admin/plugins/pluginad/house_creatives/${this.buffered.id}`,
{
type: newRecord ? "POST" : "PUT",
data,
}
);
this.savingStatus = i18n("saved");
const houseAds = this.houseAdsController.model;
if (newRecord) {
this.buffered.id = ajaxData.house_ad.id;
if (!houseAds.includes(this.buffered)) {
houseAds.pushObject(EmberObject.create(this.buffered));
}
this.router.transitionTo(
"adminPlugins.houseAds.show",
this.buffered.id
);
} else {
houseAds
.find((ad) => ad.id === this.buffered.id)
.setProperties(this.buffered);
}
} catch (error) {
popupAjaxError(error);
} finally {
this.set("model", this.buffered);
this.saving = false;
this.savingStatus = "";
}
}
}
@action
setCategoryIds(categoryArray) {
this.selectedCategories = categoryArray;
this.buffered.category_ids = categoryArray.map((c) => c.id);
this.setCategoriesForBuffered();
}
@action
setGroupIds(groupIds) {
this.selectedGroups = groupIds;
this.buffered.group_ids = groupIds.map((id) => id);
}
@action
cancel() {
this.buffered = new TrackedObject({ ...this.model });
this.selectedCategories = this.model.categories || [];
this.selectedGroups = this.model.group_ids || [];
this.setCategoriesForBuffered();
}
@action
async destroy() {
if (!this.buffered.id) {
this.router.transitionTo("adminPlugins.houseAds.index");
return;
}
try {
await ajax(
`/admin/plugins/pluginad/house_creatives/${this.buffered.id}`,
{
type: "DELETE",
}
);
this.houseAdsController.model.removeObject(
this.houseAdsController.model.findBy("id", this.buffered.id)
);
this.router.transitionTo("adminPlugins.houseAds.index");
} catch (error) {
popupAjaxError(error);
}
}
@action
openPreview() {
this.modal.show(Preview, {
model: {
html: this.buffered.html,
},
});
}
setCategoriesForBuffered() {
// we need to fetch the categories because the serializer is not being used
// to attach the category object to the house ads
this.buffered.categories = this.buffered.category_ids
? this.buffered.category_ids.map((categoryId) =>
Category.findById(categoryId)
)
: [];
}
}
@@ -0,0 +1,5 @@
import Controller from "@ember/controller";
export default class AdminPluginsHouseAdsController extends Controller {
loadingAds = true;
}
@@ -0,0 +1,12 @@
import { action } from "@ember/object";
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
export default class AdminPluginsHouseAdsIndex extends DiscourseRoute {
@service router;
@action
moreSettings() {
this.router.transitionTo("adminSiteSettingsCategory", "ad_plugin");
}
}
@@ -0,0 +1,23 @@
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
export default class AdminPluginsHouseAdsShow extends DiscourseRoute {
model(params) {
if (params.ad_id === "new") {
return new TrackedObject({
name: i18n("admin.adplugin.house_ads.new_name"),
html: "",
visible_to_logged_in_users: true,
visible_to_anons: true,
});
} else {
return new TrackedObject(
this.modelFor("adminPlugins.houseAds").findBy(
"id",
parseInt(params.ad_id, 10)
)
);
}
}
}
@@ -2,15 +2,15 @@ import EmberObject from "@ember/object";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({ export default class AdminPluginsHouseAds extends DiscourseRoute {
settings: null, settings = null;
model() { model() {
return ajax("/admin/plugins/pluginad/house_creatives.json").then((data) => { return ajax("/admin/plugins/pluginad/house_creatives.json").then((data) => {
this.set("settings", EmberObject.create(data.settings)); this.set("settings", EmberObject.create(data.settings));
return data.house_ads.map((ad) => EmberObject.create(ad)); return data.house_ads.map((ad) => EmberObject.create(ad));
}); });
}, }
setupController(controller, model) { setupController(controller, model) {
controller.setProperties({ controller.setProperties({
@@ -18,5 +18,5 @@ export default DiscourseRoute.extend({
houseAdsSettings: this.get("settings"), houseAdsSettings: this.get("settings"),
loadingAds: false, loadingAds: false,
}); });
}, }
}); }
@@ -0,0 +1,62 @@
import { LinkTo } from "@ember/routing";
import RouteTemplate from "ember-route-template";
import DButton from "discourse/components/d-button";
import routeAction from "discourse/helpers/route-action";
import { i18n } from "discourse-i18n";
import HouseAdsListSetting from "../components/house-ads-list-setting";
export default RouteTemplate(
<template>
<section class="house-ads-settings content-body">
<div>{{i18n "admin.adplugin.house_ads.description"}}</div>
{{#if @controller.houseAds.length}}
<form class="form-horizontal">
<HouseAdsListSetting
@name="topic_list_top"
@value={{@controller.adSettings.topic_list_top}}
@allAds={{@controller.houseAds}}
@adSettings={{@controller.adSettings}}
/>
<HouseAdsListSetting
@name="topic_above_post_stream"
@value={{@controller.adSettings.topic_above_post_stream}}
@allAds={{@controller.houseAds}}
@adSettings={{@controller.adSettings}}
/>
<HouseAdsListSetting
@name="topic_above_suggested"
@value={{@controller.adSettings.topic_above_suggested}}
@allAds={{@controller.houseAds}}
@adSettings={{@controller.adSettings}}
/>
<HouseAdsListSetting
@name="post_bottom"
@value={{@controller.adSettings.post_bottom}}
@allAds={{@controller.houseAds}}
@adSettings={{@controller.adSettings}}
/>
<HouseAdsListSetting
@name="topic_list_between"
@value={{@controller.adSettings.topic_list_between}}
@allAds={{@controller.houseAds}}
@adSettings={{@controller.adSettings}}
/>
<DButton
@label="admin.adplugin.house_ads.more_settings"
@icon="gear"
@action={{routeAction "moreSettings"}}
class="btn-default"
/>
</form>
{{else}}
<p>
{{#LinkTo route="adminPlugins.houseAds.show" model="new"}}
{{i18n "admin.adplugin.house_ads.get_started"}}
{{/LinkTo}}
</p>
{{/if}}
</section>
</template>
);
@@ -0,0 +1,97 @@
import { Input } from "@ember/component";
import { fn, hash } from "@ember/helper";
import RouteTemplate from "ember-route-template";
import AceEditor from "discourse/components/ace-editor";
import DButton from "discourse/components/d-button";
import TextField from "discourse/components/text-field";
import { i18n } from "discourse-i18n";
import GroupChooser from "select-kit/components/group-chooser";
import HouseAdsCategorySelector from "../components/house-ads-category-selector";
export default RouteTemplate(
<template>
<section class="edit-house-ad content-body">
<h1><TextField
@value={{@controller.buffered.name}}
class="house-ad-name"
/></h1>
<div class="controls">
<AceEditor
@content={{@controller.buffered.html}}
@onChange={{fn (mut @controller.buffered.html)}}
@mode="html"
/>
</div>
<div class="controls">
<div class="visibility-settings">
<div>
<Input
@type="checkbox"
@checked={{@controller.buffered.visible_to_logged_in_users}}
class="visible-to-logged-in-checkbox"
/>
<span>{{i18n
"admin.adplugin.house_ads.show_to_logged_in_users"
}}</span>
</div>
<div>
<Input
class="visible-to-anonymous-checkbox"
@type="checkbox"
@checked={{@controller.buffered.visible_to_anons}}
/>
<span>{{i18n "admin.adplugin.house_ads.show_to_anons"}}</span>
</div>
<HouseAdsCategorySelector
@categories={{@controller.site.categories}}
@selectedCategories={{@controller.selectedCategories}}
@onChange={{@controller.setCategoryIds}}
@options={{hash allowAny=true}}
class="house-ads-categories"
/>
<div class="description">
{{i18n "admin.adplugin.house_ads.category_chooser_description"}}
</div>
<GroupChooser
@content={{@controller.site.groups}}
@onChange={{@controller.setGroupIds}}
@value={{@controller.selectedGroups}}
class="banner-groups"
/>
<div class="description">
{{i18n "admin.adplugin.house_ads.group_chooser_description"}}
</div>
</div>
<DButton
@action={{@controller.save}}
@disabled={{@controller.disabledSave}}
@label="admin.adplugin.house_ads.save"
class="btn-primary save-button"
/>
{{#if @controller.saving}}
{{@controller.savingStatus}}
{{else}}
{{#unless @controller.disabledSave}}
<DButton @action={{@controller.cancel}} @label="cancel" />
{{/unless}}
{{/if}}
<DButton
@action={{@controller.openPreview}}
@label="admin.adplugin.house_ads.preview"
/>
<DButton
@action={{@controller.destroy}}
@label="admin.adplugin.house_ads.delete"
class="btn-danger delete-button"
/>
</div>
</section>
</template>
);
@@ -0,0 +1,43 @@
import { LinkTo } from "@ember/routing";
import RouteTemplate from "ember-route-template";
import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n";
export default RouteTemplate(
<template>
<div class="adplugin-mgmt">
<h1>{{i18n "admin.adplugin.house_ads.title"}}</h1>
{{#if @controller.model.length}}
<div class="content-list">
<div class="house-ads-actions">
<LinkTo
@route="adminPlugins.houseAds.show"
@model="new"
class="btn btn-primary"
>
{{icon "plus"}}
<span>{{i18n "admin.adplugin.house_ads.new"}}</span>
</LinkTo>
<LinkTo
@route="adminPlugins.houseAds.index"
class="btn btn-default"
>
{{icon "gear"}}
<span>{{i18n "admin.adplugin.house_ads.settings"}}</span>
</LinkTo>
</div>
<ul class="house-ads-list">
{{#each @controller.model as |ad|}}
<li class="house-ads-list-item">
{{#LinkTo route="adminPlugins.houseAds.show" model=ad.id}}
{{ad.name}}
{{/LinkTo}}
</li>
{{/each}}
</ul>
</div>
{{/if}}
{{outlet}}
</div>
</template>
);
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
#
class AdstxtController < ::ApplicationController
requires_plugin AdPlugin.plugin_name
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required
def index
raise Discourse::NotFound if SiteSetting.ads_txt.blank?
render plain: SiteSetting.ads_txt
end
end
+23 -3
View File
@@ -5,11 +5,23 @@ module ::AdPlugin
requires_plugin AdPlugin.plugin_name requires_plugin AdPlugin.plugin_name
def index def index
render_json_dump(house_ads: HouseAd.all.map(&:to_hash), settings: HouseAdSetting.all) render_json_dump(
house_ads:
HouseAd.all.map do |ad|
ad.to_hash.merge!(categories: Category.secured(@guardian).where(id: ad.category_ids))
end,
settings: HouseAdSetting.all,
)
end end
def show def show
render_json_dump(house_ad: HouseAd.find(params[:id])&.to_hash) house_ad_hash = HouseAd.find(params[:id])&.to_hash
if house_ad_hash
house_ad_hash.merge!(
categories: Category.secured(@guardian).where(id: house_ad_hash[:category_ids]),
)
end
render_json_dump(house_ad: house_ad_hash)
end end
def create def create
@@ -41,7 +53,15 @@ module ::AdPlugin
@permitted ||= @permitted ||=
begin begin
permitted = permitted =
params.permit(:id, :name, :html, :visible_to_anons, :visible_to_logged_in_users) params.permit(
:id,
:name,
:html,
:visible_to_anons,
:visible_to_logged_in_users,
group_ids: [],
category_ids: [],
)
permitted[:visible_to_logged_in_users] = ActiveModel::Type::Boolean.new.cast( permitted[:visible_to_logged_in_users] = ActiveModel::Type::Boolean.new.cast(
permitted[:visible_to_logged_in_users], permitted[:visible_to_logged_in_users],
) )
+40 -9
View File
@@ -4,7 +4,13 @@ module ::AdPlugin
class HouseAd class HouseAd
include ActiveModel::Validations include ActiveModel::Validations
attr_accessor :id, :name, :html, :visible_to_logged_in_users, :visible_to_anons attr_accessor :id,
:name,
:html,
:visible_to_logged_in_users,
:visible_to_anons,
:category_ids,
:group_ids
NAME_REGEX = /\A[[:alnum:]\s\.,'!@#$%&\*\-\+\=:]*\z/i NAME_REGEX = /\A[[:alnum:]\s\.,'!@#$%&\*\-\+\=:]*\z/i
@@ -22,6 +28,8 @@ module ::AdPlugin
@html = "<div class='house-ad'>New Ad</div>" @html = "<div class='house-ad'>New Ad</div>"
@visible_to_logged_in_users = true @visible_to_logged_in_users = true
@visible_to_anons = true @visible_to_anons = true
@group_ids = []
@category_ids = []
end end
def self.from_hash(h) def self.from_hash(h)
@@ -29,10 +37,12 @@ module ::AdPlugin
ad.name = h[:name] ad.name = h[:name]
ad.html = h[:html] ad.html = h[:html]
ad.id = h[:id].to_i if h[:id] ad.id = h[:id].to_i if h[:id]
if h.key?(:visible_to_logged_in_users) ad.visible_to_logged_in_users = h[:visible_to_logged_in_users] if h.key?(
ad.visible_to_logged_in_users = h[:visible_to_logged_in_users] :visible_to_logged_in_users,
end )
ad.visible_to_anons = h[:visible_to_anons] if h.key?(:visible_to_anons) ad.visible_to_anons = h[:visible_to_anons] if h.key?(:visible_to_anons)
ad.group_ids = h[:group_ids].map(&:to_i) if h.key?(:group_ids)
ad.category_ids = h[:category_ids].map(&:to_i) if h.key?(:category_ids)
ad ad
end end
@@ -72,8 +82,24 @@ module ::AdPlugin
self.all.select(&:visible_to_anons) self.all.select(&:visible_to_anons)
end end
def self.all_for_logged_in_users def self.all_for_logged_in_users(scope)
self.all.select(&:visible_to_logged_in_users) if scope.nil?
# this is for our admin page, so we don't need to filter by group
self.all.select(&:visible_to_logged_in_users)
else
# otherwise, filter by group and visible categories
self.all.select do |ad|
(
ad.group_ids.any? { |group_id| scope.user.groups.pluck(:id).include?(group_id) } ||
ad.group_ids.include?(Group::AUTO_GROUPS[:everyone]) || ad.group_ids.empty?
) && ad.visible_to_logged_in_users &&
(
ad.category_ids.any? do |category_id|
Category.secured(scope).pluck(:id).include?(category_id)
end || true
)
end
end
end end
def save def save
@@ -91,10 +117,13 @@ module ::AdPlugin
def update(attrs) def update(attrs)
self.name = attrs[:name] self.name = attrs[:name]
self.html = attrs[:html] self.html = attrs[:html]
if attrs.key?(:visible_to_logged_in_users) self.visible_to_logged_in_users = attrs[:visible_to_logged_in_users] if attrs.key?(
self.visible_to_logged_in_users = attrs[:visible_to_logged_in_users] :visible_to_logged_in_users,
end )
self.visible_to_anons = attrs[:visible_to_anons] if attrs.key?(:visible_to_anons) self.visible_to_anons = attrs[:visible_to_anons] if attrs.key?(:visible_to_anons)
# ensure that group_ids and category_ids can be set to an empty array
self.group_ids = attrs.key?(:group_ids) ? attrs[:group_ids].map(&:to_i) : []
self.category_ids = attrs.key?(:category_ids) ? attrs[:category_ids].map(&:to_i) : []
self.save self.save
end end
@@ -105,6 +134,8 @@ module ::AdPlugin
html: @html, html: @html,
visible_to_logged_in_users: @visible_to_logged_in_users, visible_to_logged_in_users: @visible_to_logged_in_users,
visible_to_anons: @visible_to_anons, visible_to_anons: @visible_to_anons,
group_ids: @group_ids,
category_ids: @category_ids,
} }
end end
+4 -4
View File
@@ -21,14 +21,14 @@ module ::AdPlugin
settings settings
end end
def self.settings_and_ads(for_anons: true) def self.settings_and_ads(for_anons: true, scope: nil)
settings = AdPlugin::HouseAdSetting.all settings = AdPlugin::HouseAdSetting.all
ad_names = settings.values.map { |v| v.split("|") }.flatten.uniq ad_names = settings.values.map { |v| v.split("|") }.flatten.uniq
if for_anons if for_anons
ads = AdPlugin::HouseAd.all_for_anons ads = AdPlugin::HouseAd.all_for_anons
else else
ads = AdPlugin::HouseAd.all_for_logged_in_users ads = AdPlugin::HouseAd.all_for_logged_in_users(scope)
end end
ads = ads.select { |ad| ad_names.include?(ad.name) } ads = ads.select { |ad| ad_names.include?(ad.name) }
@@ -41,14 +41,14 @@ module ::AdPlugin
), ),
creatives: creatives:
ads.inject({}) do |h, ad| ads.inject({}) do |h, ad|
h[ad.name] = ad.html h[ad.name] = { html: ad.html, category_ids: ad.category_ids }
h h
end, end,
} }
end end
def self.update(setting_name, value) def self.update(setting_name, value)
raise Discourse::NotFound unless DEFAULTS.keys.include?(setting_name.to_sym) raise Discourse::NotFound if DEFAULTS.keys.exclude?(setting_name.to_sym)
ad_names = value&.split("|") || [] ad_names = value&.split("|") || []
@@ -1,34 +1,36 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { alias, or } from "@ember/object/computed"; import { alias, or } from "@ember/object/computed";
import { inject as service } from "@ember/service"; import { service } from "@ember/service";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse/lib/decorators";
import { import {
isNthPost, isNthPost,
isNthTopicListItem, isNthTopicListItem,
} from "discourse/plugins/discourse-adplugin/discourse/helpers/slot-position"; } from "discourse/plugins/discourse-adplugin/discourse/helpers/slot-position";
export default Component.extend({ export default class AdComponent extends Component {
router: service(), @service router;
currentCategoryId: or( @or(
"router.currentRoute.attributes.category.id", "router.currentRoute.attributes.category.id",
"router.currentRoute.parent.attributes.category_id" "router.currentRoute.parent.attributes.category_id"
), )
currentCategoryId;
currentCategorySlug: or( @or(
"router.currentRoute.attributes.category.slug", "router.currentRoute.attributes.category.slug",
"router.currentRoute.parent.attributes.category.slug" "router.currentRoute.parent.attributes.category.slug"
), )
currentCategorySlug;
// Server needs to compute this in case hidden tags are being used. // Server needs to compute this in case hidden tags are being used.
topicTagsDisableAds: alias( @alias("router.currentRoute.parent.attributes.tags_disable_ads")
"router.currentRoute.parent.attributes.tags_disable_ads" topicTagsDisableAds;
),
isRestrictedCategory: or( @or(
"router.currentRoute.attributes.category.read_restricted", "router.currentRoute.attributes.category.read_restricted",
"router.currentRoute.parent.attributes.category.read_restricted" "router.currentRoute.parent.attributes.category.read_restricted"
), )
isRestrictedCategory;
@discourseComputed( @discourseComputed(
"router.currentRoute.attributes.__type", "router.currentRoute.attributes.__type",
@@ -38,33 +40,21 @@ export default Component.extend({
if (type === "tag" && tag) { if (type === "tag" && tag) {
return tag; return tag;
} }
}, }
@discourseComputed("router.currentRoute.parent.attributes.archetype") @discourseComputed("router.currentRoute.parent.attributes.archetype")
isPersonalMessage(topicType) { isPersonalMessage(topicType) {
return topicType === "private_message"; return topicType === "private_message";
}, }
@discourseComputed("currentUser.groups") @discourseComputed
showToGroups(groups) { showToGroups() {
const currentUser = this.currentUser; if (!this.currentUser) {
if (
!currentUser ||
!groups ||
!this.siteSettings.no_ads_for_groups ||
this.siteSettings.no_ads_for_groups.length === 0
) {
return true; return true;
} }
let noAdsGroups = this.siteSettings.no_ads_for_groups return this.currentUser.show_to_groups;
.split("|") }
.filter(Boolean);
let currentGroups = groups.map((g) => g.id.toString());
return !currentGroups.any((g) => noAdsGroups.includes(g));
},
@discourseComputed( @discourseComputed(
"currentCategoryId", "currentCategoryId",
@@ -94,13 +84,13 @@ export default Component.extend({
(!isRestrictedCategory || (!isRestrictedCategory ||
!this.siteSettings.no_ads_for_restricted_categories) !this.siteSettings.no_ads_for_restricted_categories)
); );
}, }
isNthPost(n) { isNthPost(n) {
return isNthPost(n, this.get("postNumber")); return isNthPost(n, this.get("postNumber"));
}, }
isNthTopicListItem(n) { isNthTopicListItem(n) {
return isNthTopicListItem(n, this.get("indexNumber")); return isNthTopicListItem(n, this.get("indexNumber"));
}, }
}); }
@@ -1,11 +1,26 @@
import { array } from "@ember/helper";
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import { service } from "@ember/service";
import { isBlank } from "@ember/utils"; import { isBlank } from "@ember/utils";
import discourseComputed, { observes } from "discourse-common/utils/decorators"; import { tagName } from "@ember-decorators/component";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component"; import discourseComputed from "discourse/lib/decorators";
import { import { isNthPost, isNthTopicListItem } from "../helpers/slot-position";
isNthPost, import AdComponent from "./ad-component";
isNthTopicListItem, import AdbutlerAd from "./adbutler-ad";
} from "discourse/plugins/discourse-adplugin/discourse/helpers/slot-position"; import AmazonProductLinks from "./amazon-product-links";
import CarbonadsAd from "./carbonads-ad";
import GoogleAdsense from "./google-adsense";
import GoogleDfpAd from "./google-dfp-ad";
import HouseAd from "./house-ad";
const AD_COMPONENTS = {
"house-ad": HouseAd,
"google-adsense": GoogleAdsense,
"google-dfp-ad": GoogleDfpAd,
"amazon-product-links": AmazonProductLinks,
"carbonads-ad": CarbonadsAd,
"adbutler-ad": AdbutlerAd,
};
const adConfig = EmberObject.create({ const adConfig = EmberObject.create({
"google-adsense": { "google-adsense": {
@@ -172,9 +187,9 @@ export function slotContenders(
return types; return types;
} }
export default AdComponent.extend({ @tagName("")
needsUpdate: false, export default class AdSlot extends AdComponent {
tagName: "", @service router;
/** /**
* For a given ad placement and optionally a post number if in between posts, * For a given ad placement and optionally a post number if in between posts,
@@ -189,30 +204,17 @@ export default AdComponent.extend({
indexNumber, indexNumber,
postNumber postNumber
); );
}, }
/**
* When house ads are configured to alternate with other ad networks, we
* need to trigger an update of which ad component is shown after
* navigating between topic lists or topics.
*/
@observes("refreshOnChange")
changed() {
if (this.get("listLoading")) {
return;
}
// force adComponents to be recomputed
this.notifyPropertyChange("needsUpdate");
},
/** /**
* Returns a list of the names of ad components that should be rendered * Returns a list of the names of ad components that should be rendered
* in the given ad placement. It handles alternating between house ads * in the given ad placement. It handles alternating between house ads
* and other ad networks. * and other ad networks.
*
* Depends on `router.currentRoute` so that we refresh ads when navigating around.
*/ */
@discourseComputed("placement", "availableAdTypes", "needsUpdate") @discourseComputed("placement", "availableAdTypes", "router.currentRoute")
adComponents(placement, availableAdTypes) { adComponentNames(placement, availableAdTypes) {
if ( if (
!availableAdTypes.includes("house-ad") || !availableAdTypes.includes("house-ad") ||
availableAdTypes.length === 1 availableAdTypes.length === 1
@@ -250,5 +252,24 @@ export default AdComponent.extend({
} }
return networkNames; return networkNames;
}, }
});
get adComponents() {
return this.adComponentNames.map((name) => AD_COMPONENTS[name]);
}
<template>
{{#each this.adComponents as |Ad|}}
{{! Trick to force full destroy/re-render of component when route changes }}
{{#each (array this.router.currentRoute)}}
<Ad
@placement={{this.placement}}
@category={{this.category}}
@postNumber={{this.postNumber}}
@indexNumber={{this.indexNumber}}
@tagName={{this.childTagName}}
/>
{{/each}}
{{/each}}
</template>
}
@@ -1,9 +1,9 @@
import { scheduleOnce } from "@ember/runloop"; import { scheduleOnce } from "@ember/runloop";
import RSVP from "rsvp"; import RSVP from "rsvp";
import discourseComputed from "discourse/lib/decorators";
import { isTesting } from "discourse/lib/environment";
import loadScript from "discourse/lib/load-script"; import loadScript from "discourse/lib/load-script";
import { isTesting } from "discourse-common/config/environment"; import AdComponent from "./ad-component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component";
let _loaded = false, let _loaded = false,
_promise = null, _promise = null,
@@ -27,9 +27,9 @@ function loadAdbutler(adserverHostname) {
return _promise; return _promise;
} }
export default AdComponent.extend({ export default class AdbutlerAd extends AdComponent {
divs: null, divs = null;
publisherId: null, publisherId = null;
init() { init() {
let dimensions = [728, 90]; let dimensions = [728, 90];
@@ -70,8 +70,8 @@ export default AdComponent.extend({
dimensions, dimensions,
}); });
this._super(); super.init();
}, }
_triggerAds() { _triggerAds() {
if (isTesting()) { if (isTesting()) {
@@ -104,52 +104,44 @@ export default AdComponent.extend({
} }
}.bind(this) }.bind(this)
); );
}, }
didInsertElement() { didInsertElement() {
this._super(); super.didInsertElement();
scheduleOnce("afterRender", this, this._triggerAds); scheduleOnce("afterRender", this, this._triggerAds);
}, }
@observes("listLoading") @discourseComputed
waitForLoad() { showAdbutlerAds() {
if (this.get("adRequested")) { if (!this.currentUser) {
return; return true;
} // already requested that this ad unit be populated
if (!this.get("listLoading")) {
scheduleOnce("afterRender", this, this._triggerAds);
} }
},
@discourseComputed("currentUser.trust_level") return this.currentUser.show_adbutler_ads;
showToTrustLevel(trustLevel) { }
return !(
trustLevel && trustLevel > this.siteSettings.adbutler_through_trust_level
);
},
@discourseComputed( @discourseComputed(
"publisherId", "publisherId",
"showToTrustLevel", "showAdbutlerAds",
"showToGroups", "showToGroups",
"showAfterPost", "showAfterPost",
"showOnCurrentPage" "showOnCurrentPage"
) )
showAd( showAd(
publisherId, publisherId,
showToTrustLevel, showAdbutlerAds,
showToGroups, showToGroups,
showAfterPost, showAfterPost,
showOnCurrentPage showOnCurrentPage
) { ) {
return ( return (
publisherId && publisherId &&
showToTrustLevel && showAdbutlerAds &&
showToGroups && showToGroups &&
showAfterPost && showAfterPost &&
showOnCurrentPage showOnCurrentPage
); );
}, }
@discourseComputed("postNumber") @discourseComputed("postNumber")
showAfterPost(postNumber) { showAfterPost(postNumber) {
@@ -157,5 +149,11 @@ export default AdComponent.extend({
return true; return true;
} }
return this.isNthPost(parseInt(this.siteSettings.adbutler_nth_post, 10)); return this.isNthPost(parseInt(this.siteSettings.adbutler_nth_post, 10));
}, }
});
<template>
{{#if this.showAd}}
<div id={{this.divId}} class={{this.className}}></div>
{{/if}}
</template>
}
@@ -1,17 +1,14 @@
import { and } from "@ember/object/computed"; import { and } from "@ember/object/computed";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import discourseComputed from "discourse-common/utils/decorators"; import { classNames } from "@ember-decorators/component";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component"; import discourseComputed from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import AdComponent from "./ad-component";
export default AdComponent.extend({ @classNames("amazon-product-links")
classNames: ["amazon-product-links"], export default class AmazonProductLinks extends AdComponent {
@and("showAmazonAds", "showToGroups", "showAfterPost", "showOnCurrentPage")
showAd: and( showAd;
"showToTrustLevel",
"showToGroups",
"showAfterPost",
"showOnCurrentPage"
),
init() { init() {
const data = { const data = {
@@ -145,40 +142,42 @@ export default AdComponent.extend({
this.set("user_input_mobile", data[placement]["user_input_mobile"]); this.set("user_input_mobile", data[placement]["user_input_mobile"]);
this.set("mobile_amazon_height", data[placement]["mobile_amazon_height"]); this.set("mobile_amazon_height", data[placement]["mobile_amazon_height"]);
this.set("mobile_amazon_width", data[placement]["mobile_amazon_width"]); this.set("mobile_amazon_width", data[placement]["mobile_amazon_width"]);
this._super(); super.init();
}, }
@discourseComputed("amazon_width", "amazon_height") @discourseComputed("amazon_width", "amazon_height")
adWrapperStyle(w, h) { adWrapperStyle(w, h) {
return htmlSafe(`width: ${w}px; height: ${h}px;`); return htmlSafe(`width: ${w}px; height: ${h}px;`);
}, }
@discourseComputed("mobile_amazon_width", "mobile_amazon_height") @discourseComputed("mobile_amazon_width", "mobile_amazon_height")
adWrapperStyleMobile(w, h) { adWrapperStyleMobile(w, h) {
return htmlSafe(`width: ${w}px; height: ${h}px;`); return htmlSafe(`width: ${w}px; height: ${h}px;`);
}, }
@discourseComputed("mobile_amazon_width") @discourseComputed("mobile_amazon_width")
adTitleStyleMobile(w) { adTitleStyleMobile(w) {
return htmlSafe(`width: ${w}px;`); return htmlSafe(`width: ${w}px;`);
}, }
@discourseComputed("user_input") @discourseComputed("user_input")
userInput(userInput) { userInput(userInput) {
return htmlSafe(`${userInput}`); return htmlSafe(`${userInput}`);
}, }
@discourseComputed("user_input_mobile") @discourseComputed("user_input_mobile")
userInputMobile(userInput) { userInputMobile(userInput) {
return htmlSafe(`${userInput}`); return htmlSafe(`${userInput}`);
}, }
@discourseComputed("currentUser.trust_level") @discourseComputed
showToTrustLevel(trustLevel) { showAmazonAds() {
return !( if (!this.currentUser) {
trustLevel && trustLevel > this.siteSettings.amazon_through_trust_level return true;
); }
},
return this.currentUser.show_amazon_ads;
}
@discourseComputed("postNumber") @discourseComputed("postNumber")
showAfterPost(postNumber) { showAfterPost(postNumber) {
@@ -187,5 +186,42 @@ export default AdComponent.extend({
} }
return this.isNthPost(parseInt(this.siteSettings.amazon_nth_post_code, 10)); return this.isNthPost(parseInt(this.siteSettings.amazon_nth_post_code, 10));
}, }
});
<template>
{{#if this.showAd}}
{{#if this.site.mobileView}}
<div
class="amazon-product-links-label"
style={{this.adTitleStyleMobile}}
><h2>{{i18n "adplugin.advertisement_label"}}</h2></div>
<iframe
style={{this.adWrapperStyleMobile}}
marginwidth="0"
marginheight="0"
scrolling="no"
frameborder="0"
src={{this.userInputMobile}}
title={{i18n "adplugin.advertisement_label"}}
>
</iframe>
{{else}}
<div class="amazon-product-links-label"><h2>{{i18n
"adplugin.advertisement_label"
}}</h2></div>
<div class="container" align="center">
<iframe
style={{this.adWrapperStyle}}
marginwidth="0"
marginheight="0"
scrolling="no"
frameborder="0"
src={{this.userInput}}
title={{i18n "adplugin.advertisement_label"}}
>
</iframe>
</div>
{{/if}}
{{/if}}
</template>
}
@@ -0,0 +1,51 @@
import { htmlSafe } from "@ember/template";
import discourseComputed from "discourse/lib/decorators";
import AdComponent from "./ad-component";
export default class CarbonadsAd extends AdComponent {
serve_id = null;
placement = null;
init() {
this.set("serve_id", this.siteSettings.carbonads_serve_id);
this.set("placement", this.siteSettings.carbonads_placement);
super.init();
}
@discourseComputed("serve_id", "placement")
url(serveId, placement) {
return htmlSafe(
`//cdn.carbonads.com/carbon.js?serve=${serveId}&placement=${placement}`
);
}
@discourseComputed
showCarbonAds() {
if (!this.currentUser) {
return true;
}
return this.currentUser.show_carbon_ads;
}
@discourseComputed(
"placement",
"serve_id",
"showCarbonAds",
"showToGroups",
"showOnCurrentPage"
)
showAd(placement, serveId, showCarbonAds, showToGroups, showOnCurrentPage) {
return (
placement && serveId && showCarbonAds && showToGroups && showOnCurrentPage
);
}
<template>
{{#if this.showAd}}
{{! template-lint-disable no-forbidden-elements }}
<script src={{this.url}} id="_carbonads_js" async type="text/javascript">
</script>
{{/if}}
</template>
}

Some files were not shown because too many files have changed in this diff Show More