+260 -6 +/-5 browse
1 | diff --git a/web/src/lists.rs b/web/src/lists.rs |
2 | index 84dee4d..a01d7f6 100644 |
3 | --- a/web/src/lists.rs |
4 | +++ b/web/src/lists.rs |
5 | @@ -17,6 +17,7 @@ |
6 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
7 | */ |
8 | |
9 | + use chrono::TimeZone; |
10 | use indexmap::IndexMap; |
11 | |
12 | use super::*; |
13 | @@ -114,7 +115,7 @@ pub async fn list( |
14 | }, |
15 | Crumb { |
16 | label: list.name.clone().into(), |
17 | - url: ListPath(list.pk().into()).to_crumb(), |
18 | + url: ListPath(list.id.to_string().into()).to_crumb(), |
19 | }, |
20 | ]; |
21 | let context = minijinja::context! { |
22 | @@ -191,11 +192,11 @@ pub async fn list_post( |
23 | }, |
24 | Crumb { |
25 | label: list.name.clone().into(), |
26 | - url: ListPath(list.pk().into()).to_crumb(), |
27 | + url: ListPath(list.id.to_string().into()).to_crumb(), |
28 | }, |
29 | Crumb { |
30 | label: format!("{} {msg_id}", subject_ref).into(), |
31 | - url: ListPostPath(list.pk().into(), msg_id.to_string()).to_crumb(), |
32 | + url: ListPostPath(list.id.to_string().into(), msg_id.to_string()).to_crumb(), |
33 | }, |
34 | ]; |
35 | let context = minijinja::context! { |
36 | @@ -298,7 +299,11 @@ pub async fn list_edit( |
37 | }, |
38 | Crumb { |
39 | label: list.name.clone().into(), |
40 | - url: ListPath(list.pk().into()).to_crumb(), |
41 | + url: ListPath(list.id.to_string().into()).to_crumb(), |
42 | + }, |
43 | + Crumb { |
44 | + label: list.name.clone().into(), |
45 | + url: ListEditPath(ListPathIdentifier::from(list.id.clone())).to_crumb(), |
46 | }, |
47 | ]; |
48 | let context = minijinja::context! { |
49 | @@ -580,3 +585,189 @@ pub async fn post_eml( |
50 | |
51 | Ok(response) |
52 | } |
53 | + |
54 | + pub async fn list_subscribers( |
55 | + ListEditSubscribersPath(id): ListEditSubscribersPath, |
56 | + mut session: WritableSession, |
57 | + auth: AuthContext, |
58 | + State(state): State<Arc<AppState>>, |
59 | + ) -> Result<Html<String>, ResponseError> { |
60 | + let db = Connection::open_db(state.conf.clone())?; |
61 | + let Some(list) = (match id { |
62 | + ListPathIdentifier::Pk(id) => db.list(id)?, |
63 | + ListPathIdentifier::Id(id) => db.list_by_id(id)?, |
64 | + }) else { |
65 | + return Err(ResponseError::new( |
66 | + "Not found".to_string(), |
67 | + StatusCode::NOT_FOUND, |
68 | + )); |
69 | + }; |
70 | + let list_owners = db.list_owners(list.pk)?; |
71 | + let user_address = &auth.current_user.as_ref().unwrap().address; |
72 | + if !list_owners.iter().any(|o| &o.address == user_address) { |
73 | + return Err(ResponseError::new( |
74 | + "Not found".to_string(), |
75 | + StatusCode::NOT_FOUND, |
76 | + )); |
77 | + }; |
78 | + |
79 | + let subs = { |
80 | + let mut stmt = db |
81 | + .connection |
82 | + .prepare("SELECT * FROM subscription WHERE list = ?;")?; |
83 | + let iter = stmt.query_map([&list.pk], |row| { |
84 | + let address: String = row.get("address")?; |
85 | + let name: Option<String> = row.get("name")?; |
86 | + let enabled: bool = row.get("enabled")?; |
87 | + let verified: bool = row.get("verified")?; |
88 | + let digest: bool = row.get("digest")?; |
89 | + let hide_address: bool = row.get("hide_address")?; |
90 | + let receive_duplicates: bool = row.get("receive_duplicates")?; |
91 | + let receive_own_posts: bool = row.get("receive_own_posts")?; |
92 | + let receive_confirmation: bool = row.get("receive_confirmation")?; |
93 | + //let last_digest: i64 = row.get("last_digest")?; |
94 | + let created: i64 = row.get("created")?; |
95 | + let last_modified: i64 = row.get("last_modified")?; |
96 | + Ok(minijinja::context! { |
97 | + address, |
98 | + name, |
99 | + enabled, |
100 | + verified, |
101 | + digest, |
102 | + hide_address, |
103 | + receive_duplicates, |
104 | + receive_own_posts, |
105 | + receive_confirmation, |
106 | + //last_digest => chrono::Utc.timestamp_opt(last_digest, 0).unwrap().to_string(), |
107 | + created => chrono::Utc.timestamp_opt(created, 0).unwrap().to_string(), |
108 | + last_modified => chrono::Utc.timestamp_opt(last_modified, 0).unwrap().to_string(), |
109 | + }) |
110 | + })?; |
111 | + let mut ret = vec![]; |
112 | + for el in iter { |
113 | + let el = el?; |
114 | + ret.push(el); |
115 | + } |
116 | + ret |
117 | + }; |
118 | + |
119 | + let crumbs = vec![ |
120 | + Crumb { |
121 | + label: "Home".into(), |
122 | + url: "/".into(), |
123 | + }, |
124 | + Crumb { |
125 | + label: list.name.clone().into(), |
126 | + url: ListPath(list.id.to_string().into()).to_crumb(), |
127 | + }, |
128 | + Crumb { |
129 | + label: list.name.clone().into(), |
130 | + url: ListEditPath(ListPathIdentifier::from(list.id.clone())).to_crumb(), |
131 | + }, |
132 | + Crumb { |
133 | + label: format!("Subscribers of {}", list.name).into(), |
134 | + url: ListEditSubscribersPath(list.id.to_string().into()).to_crumb(), |
135 | + }, |
136 | + ]; |
137 | + let context = minijinja::context! { |
138 | + site_title => state.site_title.as_ref(), |
139 | + site_subtitle => state.site_subtitle.as_ref(), |
140 | + canonical_url => ListEditSubscribersPath(ListPathIdentifier::from(list.id.clone())).to_crumb(), |
141 | + page_title => format!("Subscribers of {}", list.name), |
142 | + subs, |
143 | + root_url_prefix => &state.root_url_prefix, |
144 | + list => Value::from_object(MailingList::from(list)), |
145 | + current_user => auth.current_user, |
146 | + messages => session.drain_messages(), |
147 | + crumbs, |
148 | + }; |
149 | + Ok(Html( |
150 | + TEMPLATES.get_template("lists/subs.html")?.render(context)?, |
151 | + )) |
152 | + } |
153 | + |
154 | + pub async fn list_candidates( |
155 | + ListEditCandidatesPath(id): ListEditCandidatesPath, |
156 | + mut session: WritableSession, |
157 | + auth: AuthContext, |
158 | + State(state): State<Arc<AppState>>, |
159 | + ) -> Result<Html<String>, ResponseError> { |
160 | + let db = Connection::open_db(state.conf.clone())?; |
161 | + let Some(list) = (match id { |
162 | + ListPathIdentifier::Pk(id) => db.list(id)?, |
163 | + ListPathIdentifier::Id(id) => db.list_by_id(id)?, |
164 | + }) else { |
165 | + return Err(ResponseError::new( |
166 | + "Not found".to_string(), |
167 | + StatusCode::NOT_FOUND, |
168 | + )); |
169 | + }; |
170 | + let list_owners = db.list_owners(list.pk)?; |
171 | + let user_address = &auth.current_user.as_ref().unwrap().address; |
172 | + if !list_owners.iter().any(|o| &o.address == user_address) { |
173 | + return Err(ResponseError::new( |
174 | + "Not found".to_string(), |
175 | + StatusCode::NOT_FOUND, |
176 | + )); |
177 | + }; |
178 | + |
179 | + let subs = { |
180 | + let mut stmt = db |
181 | + .connection |
182 | + .prepare("SELECT * FROM candidate_subscription WHERE list = ?;")?; |
183 | + let iter = stmt.query_map([&list.pk], |row| { |
184 | + let address: String = row.get("address")?; |
185 | + let name: Option<String> = row.get("name")?; |
186 | + let accepted: Option<i64> = row.get("enabled")?; |
187 | + let created: i64 = row.get("created")?; |
188 | + let last_modified: i64 = row.get("last_modified")?; |
189 | + Ok(minijinja::context! { |
190 | + address, |
191 | + name, |
192 | + accepted => accepted.is_some(), |
193 | + created => chrono::Utc.timestamp_opt(created, 0).unwrap().to_string(), |
194 | + last_modified => chrono::Utc.timestamp_opt(last_modified, 0).unwrap().to_string(), |
195 | + }) |
196 | + })?; |
197 | + let mut ret = vec![]; |
198 | + for el in iter { |
199 | + let el = el?; |
200 | + ret.push(el); |
201 | + } |
202 | + ret |
203 | + }; |
204 | + |
205 | + let crumbs = vec![ |
206 | + Crumb { |
207 | + label: "Home".into(), |
208 | + url: "/".into(), |
209 | + }, |
210 | + Crumb { |
211 | + label: list.name.clone().into(), |
212 | + url: ListPath(list.id.to_string().into()).to_crumb(), |
213 | + }, |
214 | + Crumb { |
215 | + label: list.name.clone().into(), |
216 | + url: ListEditPath(ListPathIdentifier::from(list.id.clone())).to_crumb(), |
217 | + }, |
218 | + Crumb { |
219 | + label: format!("Requests of {}", list.name).into(), |
220 | + url: ListEditCandidatesPath(list.id.to_string().into()).to_crumb(), |
221 | + }, |
222 | + ]; |
223 | + let context = minijinja::context! { |
224 | + site_title => state.site_title.as_ref(), |
225 | + site_subtitle => state.site_subtitle.as_ref(), |
226 | + canonical_url => ListEditCandidatesPath(ListPathIdentifier::from(list.id.clone())).to_crumb(), |
227 | + page_title => format!("Requests of {}", list.name), |
228 | + subs, |
229 | + root_url_prefix => &state.root_url_prefix, |
230 | + list => Value::from_object(MailingList::from(list)), |
231 | + current_user => auth.current_user, |
232 | + messages => session.drain_messages(), |
233 | + crumbs, |
234 | + }; |
235 | + Ok(Html( |
236 | + TEMPLATES.get_template("lists/subs.html")?.render(context)?, |
237 | + )) |
238 | + } |
239 | diff --git a/web/src/main.rs b/web/src/main.rs |
240 | index 8f74299..4043ecf 100644 |
241 | --- a/web/src/main.rs |
242 | +++ b/web/src/main.rs |
243 | @@ -83,6 +83,20 @@ async fn main() { |
244 | Some(Arc::new("next".into())), |
245 | )), |
246 | ) |
247 | + .typed_get( |
248 | + list_subscribers.layer(RequireAuth::login_with_role_or_redirect( |
249 | + Role::User.., |
250 | + Arc::clone(&login_url), |
251 | + Some(Arc::new("next".into())), |
252 | + )), |
253 | + ) |
254 | + .typed_get( |
255 | + list_candidates.layer(RequireAuth::login_with_role_or_redirect( |
256 | + Role::User.., |
257 | + Arc::clone(&login_url), |
258 | + Some(Arc::new("next".into())), |
259 | + )), |
260 | + ) |
261 | .typed_get(help) |
262 | .typed_get(auth::ssh_signin) |
263 | .typed_post({ |
264 | diff --git a/web/src/minijinja_utils.rs b/web/src/minijinja_utils.rs |
265 | index 7a6d36a..a0fcc11 100644 |
266 | --- a/web/src/minijinja_utils.rs |
267 | +++ b/web/src/minijinja_utils.rs |
268 | @@ -45,6 +45,8 @@ lazy_static::lazy_static! { |
269 | list_path, |
270 | list_settings_path, |
271 | list_edit_path, |
272 | + list_subscribers_path, |
273 | + list_candidates_path, |
274 | list_post_path, |
275 | post_raw_path, |
276 | post_eml_path |
277 | diff --git a/web/src/templates/lists/edit.html b/web/src/templates/lists/edit.html |
278 | index 053a716..f8f28a3 100644 |
279 | --- a/web/src/templates/lists/edit.html |
280 | +++ b/web/src/templates/lists/edit.html |
281 | @@ -10,8 +10,8 @@ |
282 | {% if list.archive_url %} |
283 | <p><a href="{{ list.archive_url }}">{{ list.archive_url }}</a></p> |
284 | {% endif %} |
285 | - <p>{{ subs_count }} subscription{{ subs_count|pluralize }}.</p> |
286 | - <p>{{ sub_requests_count }} subscription request{{ sub_requests_count|pluralize }}.</p> |
287 | + <p><a href="{{ root_url_prefix }}{{ list_subscribers_path(list.pk) }}">{{ subs_count }} subscription{{ subs_count|pluralize }}.</a></p> |
288 | + <p><a href="{{ root_url_prefix }}{{ list_candidates_path(list.pk) }}">{{ sub_requests_count }} subscription request{{ sub_requests_count|pluralize }}.</a></p> |
289 | <p>{{ post_count }} post{{ post_count|pluralize }}.</p> |
290 | <form method="post" class="settings-form"> |
291 | <fieldset> |
292 | diff --git a/web/src/templates/lists/subs.html b/web/src/templates/lists/subs.html |
293 | new file mode 100644 |
294 | index 0000000..3b7cc7c |
295 | --- /dev/null |
296 | +++ b/web/src/templates/lists/subs.html |
297 | @@ -0,0 +1,47 @@ |
298 | + {% include "header.html" %} |
299 | + <div class="body body-grid"> |
300 | + <style> |
301 | + table { |
302 | + border-collapse: collapse; |
303 | + border: 2px solid rgb(200,200,200); |
304 | + letter-spacing: 1px; |
305 | + } |
306 | + |
307 | + td, th { |
308 | + border: 1px solid rgb(190,190,190); |
309 | + padding: 0.1rem 1rem; |
310 | + } |
311 | + |
312 | + th { |
313 | + background-color: var(--background-tertiary); |
314 | + } |
315 | + |
316 | + td { |
317 | + text-align: center; |
318 | + } |
319 | + |
320 | + caption { |
321 | + padding: 10px; |
322 | + } |
323 | + </style> |
324 | + <p>{{ subs|length }} entr{{ subs|length|pluralize("y","ies") }}.</a></p> |
325 | + {% if subs %} |
326 | + <div style="overflow: scroll;"> |
327 | + <table> |
328 | + <tr> |
329 | + {% for key,val in subs|first|items %} |
330 | + <th>{{ key }}</th> |
331 | + {% endfor %} |
332 | + </tr> |
333 | + {% for s in subs %} |
334 | + <tr> |
335 | + {% for key,val in s|items %} |
336 | + <td>{{ val }}</td> |
337 | + {% endfor %} |
338 | + </tr> |
339 | + {% endfor %} |
340 | + </table> |
341 | + </div> |
342 | + {% endif %} |
343 | + </div> |
344 | + {% include "footer.html" %} |