ad_space 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +22 -0
  3. data/Gemfile.lock +240 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +92 -0
  6. data/Rakefile +8 -0
  7. data/ad_space.gemspec +25 -0
  8. data/app/assets/builds/application.js +7397 -0
  9. data/app/assets/builds/application.js.map +7 -0
  10. data/app/assets/config/ad_space_manifest.js +2 -0
  11. data/app/assets/javascript/ad_space/application.js +0 -0
  12. data/app/assets/stylesheets/ad_space/application.css +15 -0
  13. data/app/controllers/ad_space/advertisements_controller.rb +121 -0
  14. data/app/controllers/ad_space/api/v1/advertisements_controller.rb +36 -0
  15. data/app/controllers/ad_space/application_controller.rb +30 -0
  16. data/app/controllers/ad_space/dashboard_controller.rb +6 -0
  17. data/app/controllers/ad_space/slots_controller.rb +70 -0
  18. data/app/helpers/ad_space/advertisements_helper.rb +4 -0
  19. data/app/helpers/ad_space/application_helper.rb +15 -0
  20. data/app/helpers/ad_space/dashboard_helper.rb +4 -0
  21. data/app/helpers/ad_space/slots_helper.rb +4 -0
  22. data/app/javascript/application.js +3 -0
  23. data/app/javascript/controllers/application.js +9 -0
  24. data/app/javascript/controllers/hello_controller.js +153 -0
  25. data/app/javascript/controllers/image_upload_controller.js +46 -0
  26. data/app/javascript/controllers/index.js +11 -0
  27. data/app/jobs/ad_space/application_job.rb +4 -0
  28. data/app/mailers/ad_space/application_mailer.rb +6 -0
  29. data/app/models/ad_space/advertisement.rb +67 -0
  30. data/app/models/ad_space/application_record.rb +5 -0
  31. data/app/models/ad_space/breadcrumb.rb +14 -0
  32. data/app/models/ad_space/slot.rb +27 -0
  33. data/app/serializers/v1/advertisement_serializer.rb +30 -0
  34. data/app/validators/url_or_custom_scheme_validator.rb +20 -0
  35. data/app/views/ad_space/advertisements/_advertisement.html.erb +102 -0
  36. data/app/views/ad_space/advertisements/_form.html.erb +143 -0
  37. data/app/views/ad_space/advertisements/edit.html.erb +10 -0
  38. data/app/views/ad_space/advertisements/index.html.erb +15 -0
  39. data/app/views/ad_space/advertisements/new.html.erb +9 -0
  40. data/app/views/ad_space/advertisements/show.html.erb +13 -0
  41. data/app/views/ad_space/dashboard/index.html.erb +7 -0
  42. data/app/views/ad_space/slots/_form.html.erb +44 -0
  43. data/app/views/ad_space/slots/_slot.html.erb +30 -0
  44. data/app/views/ad_space/slots/edit.html.erb +10 -0
  45. data/app/views/ad_space/slots/index.html.erb +14 -0
  46. data/app/views/ad_space/slots/new.html.erb +9 -0
  47. data/app/views/ad_space/slots/show.html.erb +10 -0
  48. data/app/views/layouts/ad_space/application.html.erb +43 -0
  49. data/config/routes.rb +22 -0
  50. data/db/migrate/20231026152912_create_ad_space_slots.rb +11 -0
  51. data/db/migrate/20231026160758_create_ad_space_advertisements.rb +25 -0
  52. data/db/migrate/20231027070536_add_scheme_url_to_ad_space_advertisements.rb +10 -0
  53. data/db/migrate/20231027083935_add_target_version_code_to_ad_space_advertisements.rb +6 -0
  54. data/db/migrate/20231027115707_add_is_splash_to_ad_space_slots.rb +5 -0
  55. data/lib/ad_space/app.rb +16 -0
  56. data/lib/ad_space/configuration.rb +32 -0
  57. data/lib/ad_space/engine.rb +12 -0
  58. data/lib/ad_space/version.rb +3 -0
  59. data/lib/ad_space.rb +17 -0
  60. data/lib/tasks/ad_space_tasks.rake +4 -0
  61. metadata +161 -0
@@ -0,0 +1,2 @@
1
+ //= link_directory ../stylesheets/ad_space .css
2
+ //= link_tree ../builds
File without changes
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,121 @@
1
+ module AdSpace
2
+ class AdvertisementsController < ApplicationController
3
+ before_action :set_advertisement, only: %i[ show edit update destroy remove_cover publish archive]
4
+
5
+ # GET /advertisements
6
+ def index
7
+ @advertisements = Advertisement.all
8
+
9
+ add_breadcrumb("Home", root_path)
10
+ end
11
+
12
+ # GET /advertisements/1
13
+ def show
14
+ add_breadcrumb("Home", root_path)
15
+ add_breadcrumb("Advertisements", advertisements_path)
16
+ end
17
+
18
+ # GET /advertisements/new
19
+ def new
20
+ @advertisement = Advertisement.new
21
+ end
22
+
23
+ # GET /advertisements/1/edit
24
+ def edit
25
+ end
26
+
27
+ # POST /advertisements
28
+ def create
29
+ create_params = advertisement_params.to_hash.deep_symbolize_keys
30
+ create_params[:target_version_codes] = create_params[:target_version_codes].split(',').map(&:strip).map(&:to_i).uniq.compact
31
+ @advertisement = Advertisement.new(create_params)
32
+
33
+ if @advertisement.save
34
+ redirect_to @advertisement, notice: "Advertisement was successfully created."
35
+ else
36
+ render :new, status: :unprocessable_entity
37
+ end
38
+ end
39
+
40
+ # PATCH/PUT /advertisements/1
41
+ def update
42
+ update_params = advertisement_params.to_hash.deep_symbolize_keys
43
+ update_params[:target_version_codes] = update_params[:target_version_codes].split(',').map(&:strip).map(&:to_i).uniq.compact
44
+
45
+ if @advertisement.update(update_params)
46
+ redirect_to @advertisement, notice: "Advertisement was successfully updated.", status: :see_other
47
+ else
48
+ render :edit, status: :unprocessable_entity
49
+ end
50
+ end
51
+
52
+ # DELETE /advertisements/1
53
+ def destroy
54
+ @advertisement.destroy!
55
+ redirect_to advertisements_url, notice: "Advertisement was successfully destroyed.", status: :see_other
56
+ end
57
+
58
+ def remove_cover
59
+ size = params[:size]
60
+
61
+ # 根据传递的参数来决定要删除哪个尺寸的封面图片
62
+ case size
63
+ when "sm"
64
+ @advertisement.cover_sm.purge if @advertisement.cover_sm.attached?
65
+ when "base"
66
+ @advertisement.cover_base.purge if @advertisement.cover_base.attached?
67
+ when "md"
68
+ @advertisement.cover_md.purge if @advertisement.cover_md.attached?
69
+ when "lg"
70
+ @advertisement.cover_lg.purge if @advertisement.cover_lg.attached?
71
+ else
72
+ render json: { error: "Invalid size parameter" }, status: :unprocessable_entity
73
+ return
74
+ end
75
+
76
+ render json: { success: true }
77
+ end
78
+
79
+ def publish
80
+ unless @advertisement.published?
81
+ @advertisement.update(published_at: Time.zone.now)
82
+ else
83
+ @advertisement.update(published_at: nil)
84
+ end
85
+
86
+ render turbo_stream: turbo_stream.update(
87
+ view_context.dom_id(@advertisement),
88
+ partial: "advertisement",
89
+ locals: { advertisement: @advertisement }
90
+ )
91
+ end
92
+
93
+ def archive
94
+ unless @advertisement.archived?
95
+ @advertisement.update(archived_at: Time.zone.now)
96
+ else
97
+ @advertisement.update(archived_at: nil)
98
+ end
99
+
100
+ render turbo_stream: turbo_stream.update(
101
+ view_context.dom_id(@advertisement),
102
+ partial: "advertisement",
103
+ locals: { advertisement: @advertisement }
104
+ )
105
+ end
106
+
107
+ private
108
+ # Use callbacks to share common setup or constraints between actions.
109
+ def set_advertisement
110
+ @advertisement = Advertisement.find(params[:id])
111
+ end
112
+
113
+ # Only allow a list of trusted parameters through.
114
+ def advertisement_params
115
+ params.require(:advertisement).permit(:name, :uuid, :slot_id, :published_at,
116
+ :started_at, :ended_at, :platform, :archived_at,
117
+ :weight, :remark, :cover_sm, :cover_base, :cover_md, :cover_lg,
118
+ :target_version_codes, :ad_type, :visibility, :scheme_url, :splash_ui_type, :splash_countdown, :dsp_type)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,36 @@
1
+ class AdSpace::Api::V1::AdvertisementsController < ActionController::API
2
+ # before_action :prepare_params!, only: %w[index]
3
+
4
+ def index
5
+ width = params[:screen_width].to_f
6
+ height = params[:screen_height].to_f
7
+ screen_ratio = width.zero? ? 0 : height / width
8
+ size = if screen_ratio < 1.7
9
+ "little"
10
+ elsif screen_ratio >= 1.7 && screen_ratio < 2.0
11
+ "middle"
12
+ else
13
+ "large"
14
+ end
15
+
16
+ builder = AdSpace::Advertisement.includes(:slot).where(platform: params[:platform])
17
+ builder = builder.published.unexpired
18
+ builder = builder.where(slot_id: params[:slot_ids].split(",")) unless params[:slot_ids].blank?
19
+
20
+ # TODO: 动态可配置,不同项目可以自定义特殊的查询条件,基于builder
21
+ # if current_user && current_user&.is_member?
22
+ # builder = builder.where.not(position_code: '10003')
23
+ # end
24
+
25
+ unless params[:version_code].blank?
26
+ array = [0]
27
+ array.push(params[:version_code].to_i)
28
+ codes = PG::TextEncoder::Array.new.encode(array.presence || [0]).force_encoding("utf-8")
29
+ builder = builder.where("target_version_codes && ?", codes)
30
+ end
31
+
32
+ advertisements = builder.order(weight: :desc)
33
+
34
+ render json: advertisements, each_serializer: V1::AdvertisementSerializer, size: size
35
+ end
36
+ end
@@ -0,0 +1,30 @@
1
+ module AdSpace
2
+ class ApplicationController < ActionController::Base
3
+ helper_method :_current_user
4
+
5
+ before_action :init_app
6
+
7
+ def init_app
8
+ AdSpace::App.init request: request
9
+
10
+ # 检查权限
11
+ if !_current_user&.admin?
12
+ redirect_to main_app.root_path, alert: "Permission denied - You are not authorized to access this resource.", status: :see_other
13
+ end
14
+ end
15
+
16
+ def _current_user
17
+ @user ||= User.find_by(id: session[:user_id])
18
+ end
19
+
20
+ helper_method :breadcrumbs
21
+
22
+ def breadcrumbs
23
+ @breadcrumbs ||= []
24
+ end
25
+
26
+ def add_breadcrumb(name, path = nil)
27
+ breadcrumbs << AdSpace::Breadcrumb.new(name, path)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ module AdSpace
2
+ class DashboardController < ApplicationController
3
+ def index
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,70 @@
1
+ module AdSpace
2
+ class SlotsController < ApplicationController
3
+ before_action :set_slot, only: %i[ show edit update destroy remove_demo]
4
+
5
+ # GET /slots
6
+ def index
7
+ @slots = Slot.all
8
+
9
+ add_breadcrumb("Home", root_path)
10
+ end
11
+
12
+ # GET /slots/1
13
+ def show
14
+ add_breadcrumb("Home", root_path)
15
+ add_breadcrumb("Advertisements", advertisements_path)
16
+ end
17
+
18
+ # GET /slots/new
19
+ def new
20
+ @slot = Slot.new
21
+ end
22
+
23
+ # GET /slots/1/edit
24
+ def edit
25
+ end
26
+
27
+ # POST /slots
28
+ def create
29
+ @slot = Slot.new(slot_params)
30
+
31
+ if @slot.save
32
+ redirect_to @slot, notice: "Slot was successfully created."
33
+ else
34
+ render :new, status: :unprocessable_entity
35
+ end
36
+ end
37
+
38
+ # PATCH/PUT /slots/1
39
+ def update
40
+ if @slot.update(slot_params)
41
+ redirect_to @slot, notice: "Slot was successfully updated.", status: :see_other
42
+ else
43
+ render :edit, status: :unprocessable_entity
44
+ end
45
+ end
46
+
47
+ # DELETE /slots/1
48
+ def destroy
49
+ @slot.destroy!
50
+ redirect_to slots_url, notice: "Slot was successfully destroyed.", status: :see_other
51
+ end
52
+
53
+ def remove_demo
54
+ @slot.demo.purge if @slot.demo.attached?
55
+
56
+ render json: { success: true }
57
+ end
58
+
59
+ private
60
+ # Use callbacks to share common setup or constraints between actions.
61
+ def set_slot
62
+ @slot = Slot.find(params[:id])
63
+ end
64
+
65
+ # Only allow a list of trusted parameters through.
66
+ def slot_params
67
+ params.require(:slot).permit(:name, :remark, :demo, :is_splash)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,4 @@
1
+ module AdSpace
2
+ module AdvertisementsHelper
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ module AdSpace
2
+ module ApplicationHelper
3
+ def user_identifier(user)
4
+ if user.respond_to?(:email) && user.email.present?
5
+ user.email
6
+ elsif user.respond_to?(:username) && user.username.present?
7
+ user.username.camelize
8
+ elsif user.respond_to?(:name) && user.name.present?
9
+ user.name.camelize
10
+ else
11
+ user.id
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ module AdSpace
2
+ module DashboardHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module AdSpace
2
+ module SlotsHelper
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ // Entry point for the build script in your package.json
2
+ import "@hotwired/turbo-rails"
3
+ import "./controllers"
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+
3
+ const application = Application.start()
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false
7
+ window.Stimulus = application
8
+
9
+ export { application }
@@ -0,0 +1,153 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ let isShowSplashInputs = false;
4
+
5
+ export default class extends Controller {
6
+ // 默认是dsp广告
7
+ static values = {
8
+ isDspAd: {type: Boolean, default: true},
9
+ isSplash: {type: Boolean, default: false}
10
+ }
11
+ static targets = [ "adType", "slot", "splashUiType", "splashCountdown", "smCover", "mdCover", "lgCover", "baseCover", "schemeUrl" ]
12
+
13
+ connect() {
14
+ this.render()
15
+ }
16
+
17
+ handleAdTypeChnage(event) {
18
+ this.isDspAdValue = event.target.value == 'dsp'
19
+ this.render()
20
+ }
21
+
22
+ handleSlotChange(event) {
23
+ const isSplash = event.target[event.target.selectedIndex].getAttribute('data-is-splash')
24
+ this.isSplashValue = isSplash === "true"
25
+ this.render()
26
+ }
27
+
28
+ render() {
29
+ if (this.isSplashValue) {
30
+ if (this.isDspAdValue) {
31
+ // 开屏且为dsp类广告,隐藏除倒计时、ui类型之外的参数
32
+ this.schemeUrlTarget.style.display = "none";
33
+ const schemeUrlElement = this.schemeUrlTarget.querySelector("input[type='text']");
34
+ if (schemeUrlElement) {
35
+ schemeUrlElement.value = null;
36
+ }
37
+
38
+ this.smCoverTarget.style.display = "none";
39
+ const smCoverElement = this.smCoverTarget.querySelector("input[type='file']");
40
+ if (smCoverElement) {
41
+ smCoverElement.value = null;
42
+ }
43
+
44
+ this.mdCoverTarget.style.display = "none";
45
+ const mdCoverElement = this.mdCoverTarget.querySelector("input[type='file']");
46
+ if (mdCoverElement) {
47
+ mdCoverElement.value = null;
48
+ }
49
+
50
+ this.lgCoverTarget.style.display = "none";
51
+ const lgCoverElement = this.lgCoverTarget.querySelector("input[type='file']");
52
+ if (lgCoverElement) {
53
+ lgCoverElement.value = null;
54
+ }
55
+
56
+ this.baseCoverTarget.style.display = "none";
57
+ const baseCoverElement = this.baseCoverTarget.querySelector("input[type='file']");
58
+ if (baseCoverElement) {
59
+ baseCoverElement.value = null;
60
+ }
61
+
62
+ this.splashCountdownTarget.style.display = "block";
63
+ this.splashUiTypeTarget.style.display = "block";
64
+ } else {
65
+ // 非dsp类广告,显示所有自定义元素
66
+ this.splashCountdownTarget.style.display = "block";
67
+ this.splashUiTypeTarget.style.display = "block";
68
+ this.smCoverTarget.style.display = "block";
69
+ this.mdCoverTarget.style.display = "block";
70
+ this.lgCoverTarget.style.display = "block";
71
+ this.baseCoverTarget.style.display = "block";
72
+ this.schemeUrlTarget.style.display = "block";
73
+ }
74
+ } else {
75
+ if (this.isDspAdValue) {
76
+ // 非开屏且为dsp类型广告,隐藏所有开屏参数
77
+ this.schemeUrlTarget.style.display = "none";
78
+ const schemeUrlElement = this.schemeUrlTarget.querySelector("input[type='text']");
79
+ if (schemeUrlElement) {
80
+ schemeUrlElement.value = null;
81
+ }
82
+
83
+ this.splashCountdownTarget.style.display = "none";
84
+ const splashCountdownElement = this.splashCountdownTarget.querySelector("input[type='number']");
85
+ if (splashCountdownElement) {
86
+ splashCountdownElement.value = 5;
87
+ }
88
+
89
+ this.splashUiTypeTarget.style.display = "none";
90
+ const splashUiTypeElement = this.splashUiTypeTarget.querySelector("select");
91
+ if (splashUiTypeElement) {
92
+ splashUiTypeElement.value = "half";
93
+ }
94
+
95
+ this.smCoverTarget.style.display = "none";
96
+ const smCoverElement = this.smCoverTarget.querySelector("input[type='file']");
97
+ if (smCoverElement) {
98
+ smCoverElement.value = null;
99
+ }
100
+
101
+ this.mdCoverTarget.style.display = "none";
102
+ const mdCoverElement = this.mdCoverTarget.querySelector("input[type='file']");
103
+ if (mdCoverElement) {
104
+ mdCoverElement.value = null;
105
+ }
106
+
107
+ this.lgCoverTarget.style.display = "none";
108
+ const lgCoverElement = this.lgCoverTarget.querySelector("input[type='file']");
109
+ if (lgCoverElement) {
110
+ lgCoverElement.value = null;
111
+ }
112
+
113
+ this.baseCoverTarget.style.display = "none";
114
+ const baseCoverElement = this.baseCoverTarget.querySelector("input[type='file']");
115
+ if (baseCoverElement) {
116
+ baseCoverElement.value = null;
117
+ }
118
+ } else {
119
+ // 非dsp、非开屏,保留schemeUrl、baseCover,其它类型都隐藏
120
+ this.schemeUrlTarget.style.display = "block";
121
+ this.baseCoverTarget.style.display = "block";
122
+
123
+ this.splashCountdownTarget.style.display = "none";
124
+ const splashCountdownElement = this.splashCountdownTarget.querySelector("input[type='number']");
125
+ if (splashCountdownElement) {
126
+ splashCountdownElement.value = 5;
127
+ }
128
+
129
+ this.splashUiTypeTarget.style.display = "none";
130
+ const splashUiTypeElement = this.splashUiTypeTarget.querySelector("select");
131
+ if (splashUiTypeElement) {
132
+ splashUiTypeElement.value = "half";
133
+ }
134
+ this.smCoverTarget.style.display = "none";
135
+ const smCoverElement = this.smCoverTarget.querySelector("input[type='file']");
136
+ if (smCoverElement) {
137
+ smCoverElement.value = null;
138
+ }
139
+
140
+ this.mdCoverTarget.style.display = "none";
141
+ const mdCoverElement = this.mdCoverTarget.querySelector("input[type='file']");
142
+ if (mdCoverElement) {
143
+ mdCoverElement.value = null;
144
+ }
145
+ this.lgCoverTarget.style.display = "none";
146
+ const lgCoverElement = this.lgCoverTarget.querySelector("input[type='file']");
147
+ if (lgCoverElement) {
148
+ lgCoverElement.value = null;
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,46 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { destroy } from '@rails/request.js'
3
+
4
+ // Connects to data-controller="image-upload"
5
+ export default class extends Controller {
6
+ static targets = ["input", "preview", "removeButton"]
7
+ static values = {
8
+ url: {type: String, default: null},
9
+ }
10
+
11
+ connect() {
12
+ this.inputTarget.addEventListener("change", this.preview.bind(this))
13
+ this.removeButtonTarget.addEventListener("click", this.remove.bind(this))
14
+ }
15
+
16
+ preview() {
17
+ const file = this.inputTarget.files[0]
18
+
19
+ if (file) {
20
+ const reader = new FileReader()
21
+ reader.onload = (event) => {
22
+ this.previewTarget.src = event.target.result
23
+ this.removeButtonTarget.style.display = "block" // 显示移除按钮
24
+ }
25
+ reader.readAsDataURL(file)
26
+ } else {
27
+ this.previewTarget.src = ""
28
+ this.removeButtonTarget.style.display = "none" // 隐藏移除按钮
29
+ }
30
+ }
31
+
32
+ async remove() {
33
+ if (this.urlValue) {
34
+ const response = await destroy(this.urlValue)
35
+ if (response.ok) {
36
+ this.inputTarget.value = "" // 清空文件输入框的值
37
+ this.previewTarget.src = "" // 清空预览
38
+ this.removeButtonTarget.style.display = "none" // 隐藏移除按钮
39
+ }
40
+ } else {
41
+ this.inputTarget.value = "" // 清空文件输入框的值
42
+ this.previewTarget.src = "" // 清空预览
43
+ this.removeButtonTarget.style.display = "none" // 隐藏移除按钮
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,11 @@
1
+ // This file is auto-generated by ./bin/rails stimulus:manifest:update
2
+ // Run that command whenever you add a new controller or create them with
3
+ // ./bin/rails generate stimulus controllerName
4
+
5
+ import { application } from "./application"
6
+
7
+ import HelloController from "./hello_controller"
8
+ application.register("hello", HelloController)
9
+
10
+ import ImageUploadController from "./image_upload_controller"
11
+ application.register("image-upload", ImageUploadController)
@@ -0,0 +1,4 @@
1
+ module AdSpace
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module AdSpace
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,67 @@
1
+ module AdSpace
2
+ class Advertisement < ApplicationRecord
3
+ before_validation :generate_uuid, on: :create
4
+
5
+ enum platform: { ios: 0, android: 1 }, _suffix: true
6
+ enum ad_type: { dsp: 0, custom: 1 }, _suffix: true
7
+ enum splash_ui_type: { half: 0, full: 1 }, _suffix: true
8
+ enum visibility: { all: 0, regular_user: 2, premium_member: 1 }, _suffix: true
9
+ enum dsp_type: { gdt: 0, jrtt: 1, google: 2 }, _suffix: true
10
+
11
+ validates :name, presence: true, uniqueness: true
12
+ validates :uuid, presence: true, uniqueness: true
13
+ validates :ad_type, presence: true
14
+
15
+ validates :started_at, presence: true
16
+ validates :ended_at, presence: true
17
+ validate :valid_date_range
18
+
19
+ validates :platform, presence: true, inclusion: { in: platforms.keys }
20
+ validates :scheme_url, allow_blank: true, url_or_custom_scheme: true, if: -> { custom_ad_type? }
21
+
22
+ belongs_to :slot, class_name: "AdSpace::Slot", foreign_key: "slot_id"
23
+ delegate :splash?, to: :slot, prefix: false
24
+
25
+ has_one_attached :cover_sm, dependent: :destroy
26
+ has_one_attached :cover_base, dependent: :destroy
27
+ has_one_attached :cover_md, dependent: :destroy
28
+ has_one_attached :cover_lg, dependent: :destroy
29
+
30
+ scope :published, -> { where.not(published_at: nil) }
31
+ scope :unexpired, -> { where("started_at <= ?", Time.zone.now + 15.days).where("ended_at >= ?", Time.zone.now) }
32
+
33
+ def published?
34
+ published_at.present?
35
+ end
36
+
37
+ def archived?
38
+ archived_at.present?
39
+ end
40
+
41
+ def cover_url(size = nil)
42
+ if object.splash?
43
+ return cover_sm.attached? ? cover_sm.url : nil if size == 'little'
44
+ return cover_md.attached? ? cover_md.url : nil if size == 'middle'
45
+ return cover_lg.attached? ? cover_lg.url : nil if size == 'large'
46
+ else
47
+ cover_base.attached? ? cover_base.url : nil
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def valid_date_range
54
+ if started_at.present? && ended_at.present? && started_at > ended_at
55
+ errors.add(:ended_at, "must be greater than or equal to started_at")
56
+ end
57
+ end
58
+
59
+ def generate_uuid
60
+ if self.uuid.blank?
61
+ begin
62
+ self.uuid = SecureRandom.uuid
63
+ end while self.class.exists?(uuid: uuid)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ module AdSpace
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ module AdSpace
2
+ class Breadcrumb
3
+ attr_reader :name, :path
4
+
5
+ def initialize(name, path)
6
+ @name = name
7
+ @path = path
8
+ end
9
+
10
+ def link?
11
+ @path.present?
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ module AdSpace
2
+ class Slot < ApplicationRecord
3
+ has_many :advertisements, dependent: :destroy
4
+
5
+ validates :name, presence: true, uniqueness: true
6
+ validates :uuid, presence: true, uniqueness: true
7
+ validates :remark, length: { maximum: 250 }
8
+
9
+ before_validation :generate_uuid, on: :create
10
+
11
+ has_one_attached :demo, dependent: :destroy
12
+
13
+ def splash?
14
+ is_splash
15
+ end
16
+
17
+ private
18
+
19
+ def generate_uuid
20
+ if self.uuid.blank?
21
+ begin
22
+ self.uuid = SecureRandom.uuid
23
+ end while self.class.exists?(uuid: uuid)
24
+ end
25
+ end
26
+ end
27
+ end