Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: 6cae75e5aeedfe0a4b674b66a61f5c6c13b18adb
Timestamp: Mon, 08 May 2023 08:45:29 +0000 (1 year ago)

+260 -6 +/-5 browse
web: expand list owner edit view
1diff --git a/web/src/lists.rs b/web/src/lists.rs
2index 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
240index 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
265index 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
278index 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
293new file mode 100644
294index 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" %}