Skip to content

Commit 721cb05

Browse files
authored
feat: open partners escalations data (#99)
* feat: add partners escalations schema0, migrations, and scripts * feat: add other sources UI * fix: resolve review comments
1 parent e2f885d commit 721cb05

29 files changed

Lines changed: 1582 additions & 12 deletions

config/config.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ config :sentry,
7373

7474
config :dau, AWSS3, file_prefix: "temp"
7575

76-
config :dau, preview_dataset_base_s3_url: "https://s3.ap-south-1.amazonaws.com/public.dau.tattle.co.in/preview_dataset/media/"
76+
config :dau, preview_tipline_dataset_base_s3_url: "https://s3.ap-south-1.amazonaws.com/public.dau.tattle.co.in/preview_dataset/media/"
77+
78+
config :dau, preview_other_sources_dataset_base_s3_url: "https://s3.ap-south-1.amazonaws.com/public.dau.tattle.co.in/preview_dataset/escalations/"
7779

7880
# Import environment specific config. This must remain at the bottom
7981
# of this file so it overrides the configuration defined above.

lib/dau/open_data.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,4 +568,8 @@ defmodule DAU.OpenData do
568568
|> preload(tags: ^from(t in Tag, order_by: [asc: t.name]))
569569
|> Repo.all()
570570
end
571+
572+
def list_languages_tags() do
573+
Tag |> where([t], like(t.slug, "language_%") and t.slug != "language_other") |> Repo.all()
574+
end
571575
end

lib/dau/open_data/assessment_report.ex

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ defmodule DAU.OpenData.AssessmentReport do
1010
:language_of_content,
1111
:tools_used,
1212
:observations,
13-
:remarks
13+
:remarks,
14+
:uuid
15+
]
16+
17+
@required_fields [
18+
:assessment_report_link,
1419
]
1520

1621
schema "assessment_reports" do
@@ -22,17 +27,23 @@ defmodule DAU.OpenData.AssessmentReport do
2227
field :tools_used, {:array, :string}
2328
field :observations, {:array, :string}
2429
field :remarks, :string
30+
field :uuid, Ecto.UUID
2531

2632
many_to_many :commons, DAU.Feed.Common, join_through: DAU.OpenData.CommonAssessmentReport
33+
many_to_many :tags, DAU.OpenData.Tag, join_through: DAU.OpenData.AssessmentReportTag
2734

2835
timestamps(type: :utc_datetime)
2936
end
3037

3138
def changeset(assessment_report, attrs \\ %{}) do
3239
assessment_report
3340
|> cast(attrs, @fields)
41+
|> validate_required(@required_fields)
3442
|> unique_constraint(:assessment_report_link,
3543
name: :assessment_reports_assessment_report_link_unique_index
3644
)
45+
|> unique_constraint(:uuid,
46+
name: :assessment_reports_uuid_unique_index
47+
)
3748
end
3849
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule DAU.OpenData.AssessmentReportTag do
2+
use Ecto.Schema
3+
import Ecto.Changeset
4+
5+
@primary_key false
6+
schema "assessment_report_tags" do
7+
belongs_to :assessment_report, DAU.OpenData.AssessmentReport
8+
belongs_to :tag, DAU.OpenData.Tag
9+
10+
timestamps(type: :utc_datetime)
11+
end
12+
13+
def changeset(assessment_report_tag, attrs) do
14+
assessment_report_tag
15+
|> cast(attrs, [:assessment_report_id, :tag_id])
16+
|> validate_required([:assessment_report_id, :tag_id])
17+
|> assoc_constraint(:assessment_report)
18+
|> assoc_constraint(:tag)
19+
|> unique_constraint([:assessment_report_id, :tag_id],
20+
name: :assessment_report_tags_unique_index
21+
)
22+
end
23+
end

lib/dau/open_data/feed_open_query.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ defmodule DAU.OpenData.FeedOpenQuery do
155155
defp present_filter(value), do: value
156156

157157
defp bulk_add_s3_media_url(common_rows) do
158-
base_preview_url = Application.fetch_env!(:dau, :preview_dataset_base_s3_url)
158+
base_preview_url = Application.fetch_env!(:dau, :preview_tipline_dataset_base_s3_url)
159159
Enum.map(common_rows, fn query ->
160160
media_type = query.media_type
161161

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
defmodule DAU.OpenData.OtherSourcesOpenQuery do
2+
import Ecto.Query, warn: false
3+
alias DAU.OpenData.PartnerEscalation
4+
alias DAU.OpenData.AssessmentReport
5+
alias DAU.OpenData.AssessmentReportTag
6+
alias DAU.OpenData.PartnerEscalationTag
7+
alias DAU.OpenData.Tag
8+
9+
alias DAU.Repo
10+
11+
@page_size 25
12+
13+
def list_combined_data(search_params) do
14+
order = %{
15+
"newest" => [desc: :date],
16+
"oldest" => [asc: :date]
17+
}
18+
19+
tag_filter = present_filter(Keyword.get(search_params, :tag))
20+
21+
query =
22+
make_base_query(tag_filter)
23+
# Wrap the union so we can safely order/limit/offset on combined fields.
24+
|> subquery()
25+
|> maybe_filter_from_date(Keyword.get(search_params, :from))
26+
|> maybe_filter_to_date(Keyword.get(search_params, :to))
27+
28+
count = Repo.aggregate(query, :count, :uuid)
29+
30+
results =
31+
query
32+
|> order_by(^Map.get(order, Keyword.get(search_params, :sort), desc: :date))
33+
|> limit(^@page_size)
34+
|> offset(^(@page_size * (Keyword.get(search_params, :page_num, 1) - 1)))
35+
|> Repo.all()
36+
|> bulk_add_s3_media_url()
37+
38+
{count, results}
39+
end
40+
41+
defp make_base_query(tag_filter) do
42+
# Build partner rows with tags aggregated into a JSON array per row.
43+
query_partners =
44+
PartnerEscalation
45+
# Left join keeps rows even when no tags exist.
46+
|> join(:left, [row], t in assoc(row, :tags))
47+
|> where([row], row.media_urls != [] and not is_nil(row.media_urls))
48+
|> where(
49+
[row],
50+
fragment(
51+
"EXISTS (SELECT 1 FROM unnest(?) AS u(url) WHERE u.url ~* ?)",
52+
row.media_urls,
53+
"^(https?://)"
54+
)
55+
)
56+
|> maybe_filter_tags_for_partner(tag_filter)
57+
# Group by row.id so the tag join doesn't multiply rows.
58+
|> group_by([row], row.id)
59+
|> select([row, t], %{
60+
type: "partner",
61+
date: row.date,
62+
preview_url: nil,
63+
assessment_report_link: nil,
64+
count: row.count,
65+
tools_used: row.tools_used,
66+
observations: row.observations,
67+
uuid: row.uuid,
68+
# Aggregate tag slug+name into a JSONB array; empty array when no tags.
69+
tags:
70+
fragment(
71+
"coalesce(jsonb_agg(DISTINCT jsonb_build_object('slug', ?, 'name', ?)) FILTER (WHERE ? IS NOT NULL), '[]'::jsonb)",
72+
t.slug,
73+
t.name,
74+
t.slug
75+
)
76+
})
77+
78+
# Build assessment report rows with tags aggregated into a JSON array per row.
79+
query_reports =
80+
AssessmentReport
81+
# Left join keeps rows even when no tags exist.
82+
|> join(:left, [row], t in assoc(row, :tags))
83+
|> where([row], row.media_urls != [] and not is_nil(row.media_urls))
84+
|> where(
85+
[row],
86+
fragment(
87+
"EXISTS (SELECT 1 FROM unnest(?) AS u(url) WHERE u.url ~* ?)",
88+
row.media_urls,
89+
"^(https?://)"
90+
)
91+
)
92+
|> maybe_filter_tags_for_report(tag_filter)
93+
# Group by row.id so the tag join doesn't multiply rows.
94+
|> group_by([row], row.id)
95+
|> select([row, t], %{
96+
type: "assessment_report",
97+
date: row.date,
98+
preview_url: nil,
99+
assessment_report_link: row.assessment_report_link,
100+
count: nil,
101+
tools_used: row.tools_used,
102+
observations: row.observations,
103+
uuid: row.uuid,
104+
# Aggregate tag slug+name into a JSONB array; empty array when no tags.
105+
tags:
106+
fragment(
107+
"coalesce(jsonb_agg(DISTINCT jsonb_build_object('slug', ?, 'name', ?)) FILTER (WHERE ? IS NOT NULL), '[]'::jsonb)",
108+
t.slug,
109+
t.name,
110+
t.slug
111+
)
112+
})
113+
114+
union_all(query_partners, ^query_reports)
115+
end
116+
117+
def page_size, do: @page_size
118+
119+
defp maybe_filter_from_date(query, nil), do: query
120+
121+
defp maybe_filter_from_date(query, date_string) do
122+
case Date.from_iso8601(date_string) do
123+
{:ok, date} ->
124+
where(query, [c], c.date >= ^date)
125+
126+
_ ->
127+
query
128+
end
129+
end
130+
131+
defp maybe_filter_to_date(query, nil), do: query
132+
133+
defp maybe_filter_to_date(query, date_string) do
134+
case Date.from_iso8601(date_string) do
135+
{:ok, date} ->
136+
where(query, [c], c.date <= ^date)
137+
138+
_ ->
139+
query
140+
end
141+
end
142+
143+
defp maybe_filter_tags_for_partner(query, nil), do: query
144+
145+
defp maybe_filter_tags_for_partner(query, tag_slug) do
146+
# Filter rows by tag without shrinking the aggregated tag list.
147+
148+
#explaination in below function
149+
where(query, [row, _t],
150+
fragment(
151+
"EXISTS (SELECT 1 FROM partner_escalation_tags pet JOIN tags t ON t.id = pet.tag_id WHERE pet.partner_escalation_id = ? AND t.slug = ?)",
152+
row.id,
153+
^tag_slug
154+
)
155+
)
156+
end
157+
158+
defp maybe_filter_tags_for_report(query, nil), do: query
159+
160+
defp maybe_filter_tags_for_report(query, tag_slug) do
161+
# Filter rows by tag without shrinking the aggregated tag list.
162+
163+
# The outer query has row (report) and t (joined tags for aggregation).
164+
# The WHERE EXISTS (...) is evaluated for each row.
165+
# Inside the EXISTS, we join the report‑tag join table to tags, and check:
166+
# assessment_report_id = row.id
167+
# tag.slug = desired slug
168+
# If a match exists → keep that row in the outer query.
169+
# If not → that row is filtered out.
170+
171+
# The issue with the normal filtering is that this is happening before we are aggregating the tags
172+
# in the select. So, if a row had 3 tags t1, t2, t3. In normal filtering for t1, the
173+
# rows with t2, t3 will get discarded from the join. This would result in rows still being
174+
# properly filtered, but in the tags field of rows, we would have not gotten all the tags for the row (
175+
# because they were deleted from the join table earlier). therefor this approach.
176+
177+
where(query, [row, _t],
178+
fragment(
179+
"EXISTS (SELECT 1 FROM assessment_report_tags art JOIN tags t ON t.id = art.tag_id WHERE art.assessment_report_id = ? AND t.slug = ?)",
180+
row.id,
181+
^tag_slug
182+
)
183+
)
184+
end
185+
186+
defp present_filter(value) when value in [nil, "", "all"], do: nil
187+
defp present_filter(value), do: value
188+
189+
defp bulk_add_s3_media_url(rows) do
190+
base_preview_url = Application.fetch_env!(:dau, :preview_other_sources_dataset_base_s3_url)
191+
192+
Enum.map(rows, fn row ->
193+
Map.put(row, :preview_url, base_preview_url <> "thumbnail_" <> row.uuid <> ".png")
194+
end)
195+
end
196+
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
defmodule DAU.OpenData.PartnerEscalation do
2+
use Ecto.Schema
3+
import Ecto.Changeset
4+
5+
@fields [
6+
:date,
7+
:media_urls,
8+
:count,
9+
:language_of_content,
10+
:tools_used,
11+
:observations,
12+
:remarks,
13+
:uuid
14+
]
15+
16+
schema "partner_escalations" do
17+
field :date, :date
18+
field :media_urls, {:array, :string}
19+
field :count, :integer
20+
field :language_of_content, :string
21+
field :tools_used, {:array, :string}
22+
field :observations, {:array, :string}
23+
field :remarks, :string
24+
field :uuid, Ecto.UUID
25+
26+
many_to_many :tags, DAU.OpenData.Tag, join_through: DAU.OpenData.PartnerEscalationTag
27+
28+
timestamps(type: :utc_datetime)
29+
end
30+
31+
def changeset(partner_escalation, attrs \\ %{}) do
32+
partner_escalation
33+
|> cast(attrs, @fields)
34+
|> validate_required([:uuid])
35+
|> unique_constraint(:uuid, name: :partner_escalations_uuid_unique_index)
36+
end
37+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule DAU.OpenData.PartnerEscalationTag do
2+
use Ecto.Schema
3+
import Ecto.Changeset
4+
5+
@primary_key false
6+
schema "partner_escalation_tags" do
7+
belongs_to :partner_escalation, DAU.OpenData.PartnerEscalation
8+
belongs_to :tag, DAU.OpenData.Tag
9+
10+
timestamps(type: :utc_datetime)
11+
end
12+
13+
def changeset(partner_escalation_tag, attrs) do
14+
partner_escalation_tag
15+
|> cast(attrs, [:partner_escalation_id, :tag_id])
16+
|> validate_required([:partner_escalation_id, :tag_id])
17+
|> assoc_constraint(:partner_escalation)
18+
|> assoc_constraint(:tag)
19+
|> unique_constraint([:partner_escalation_id, :tag_id],
20+
name: :partner_escalation_tags_unique_index
21+
)
22+
end
23+
end

lib/dau/open_data/tag.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ defmodule DAU.OpenData.Tag do
88
belongs_to :tags_category, DAU.OpenData.TagsCategory
99

1010
many_to_many :commons, DAU.Feed.Common, join_through: DAU.OpenData.CommonTag
11+
many_to_many :assessment_reports, DAU.OpenData.AssessmentReport,
12+
join_through: DAU.OpenData.AssessmentReportTag
13+
many_to_many :partner_escalations, DAU.OpenData.PartnerEscalation,
14+
join_through: DAU.OpenData.PartnerEscalationTag
1115

1216
timestamps(type: :utc_datetime)
1317
end

lib/dau_web/live/feed_open_live.ex renamed to lib/dau_web/live/open_data_live/feed_open_live.ex

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
defmodule DAUWeb.FeedOpenLive do
2-
import DAUWeb.FeedOpenLive.SearchParams
1+
defmodule DAUWeb.OpenDataLive.FeedOpenLive do
2+
import DAUWeb.OpenDataLive.FeedOpenSearchParams
33
import DAUWeb.Components.OpenDataComponents
4-
alias DAUWeb.FeedOpenLive.SearchParams
4+
alias DAUWeb.OpenDataLive.FeedOpenSearchParams, as: SearchParams
55
alias DAU.OpenData
66
alias DAU.OpenData.FeedOpenQuery
7-
alias DAU.Accounts
87
use DAUWeb, :live_view
98
use DAUWeb, :html
109

0 commit comments

Comments
 (0)