ad_space 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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