make details for Comic, Artist and Issue clickable, add CustomField to select Comic and Issue
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Any
|
||||||
from natsort import natsorted
|
from natsort import natsorted
|
||||||
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
|
from sqlalchemy import Column, ForeignKey, Integer, String, Boolean, func
|
||||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||||
@@ -48,10 +48,10 @@ class Comic(Base, BaseMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.title}({self.id})'
|
return f'{self.title}({self.id})'
|
||||||
|
|
||||||
def get_artists(self) -> Dict[str, List[str]]:
|
def get_artists(self) -> Dict[Any, List[Any]]:
|
||||||
works: Dict[str, List[str]] = {}
|
works: Dict[Any, List[Any]] = {}
|
||||||
for work in self.comic_works:
|
for work in self.comic_works:
|
||||||
work_type = work.work_type.name
|
work_type = work.work_type
|
||||||
artist = work.artist
|
artist = work.artist
|
||||||
if work_type in works:
|
if work_type in works:
|
||||||
works[work_type].append(artist)
|
works[work_type].append(artist)
|
||||||
@@ -107,6 +107,18 @@ class Issue(Base, BaseMixin):
|
|||||||
story_arc = relationship("StoryArc", back_populates="issues")
|
story_arc = relationship("StoryArc", back_populates="issues")
|
||||||
issue_works = relationship("IssueWork")
|
issue_works = relationship("IssueWork")
|
||||||
|
|
||||||
|
def get_artists(self) -> Dict[Any, List[Any]]:
|
||||||
|
works: Dict[Any, List[Any]] = {}
|
||||||
|
for work in self.issue_works:
|
||||||
|
work_type = work.work_type
|
||||||
|
artist = work.artist
|
||||||
|
if work_type in works:
|
||||||
|
works[work_type].append(artist)
|
||||||
|
else:
|
||||||
|
works[work_type] = [artist]
|
||||||
|
return works
|
||||||
|
|
||||||
|
|
||||||
class Artist(Base, BaseMixin):
|
class Artist(Base, BaseMixin):
|
||||||
__tablename__ = "artist"
|
__tablename__ = "artist"
|
||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
@@ -114,10 +126,10 @@ class Artist(Base, BaseMixin):
|
|||||||
comic_works = relationship("ComicWork")
|
comic_works = relationship("ComicWork")
|
||||||
issue_works = relationship("IssueWork")
|
issue_works = relationship("IssueWork")
|
||||||
|
|
||||||
def get_comics(self) -> Dict[str, List[str]]:
|
def get_comics(self) -> Dict[Any, List[Comic]]:
|
||||||
works: Dict[str, List[str]] = {}
|
works: Dict[Any, List[Comic]] = {}
|
||||||
for work in self.comic_works:
|
for work in self.comic_works:
|
||||||
work_type = work.work_type.name
|
work_type = work.work_type
|
||||||
comic = work.comic
|
comic = work.comic
|
||||||
if work_type in works:
|
if work_type in works:
|
||||||
works[work_type].append(comic)
|
works[work_type].append(comic)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
{% for work in artist.get_comics() %}
|
{% for work in artist.get_comics() %}
|
||||||
<p>
|
<p>
|
||||||
{{work}}:
|
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||||
<ul>
|
<ul>
|
||||||
{% for comic in artist.get_comics()[work] %}
|
{% for comic in artist.get_comics()[work] %}
|
||||||
<li><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></li>
|
<li><a href="/comic/comics/{{comic.id}}">{{comic.title}}</a></li>
|
||||||
@@ -47,6 +47,10 @@
|
|||||||
<th scope="row">Data Modified</th>
|
<th scope="row">Data Modified</th>
|
||||||
<td colspan="2">{{artist.last_modified_date}}</td>
|
<td colspan="2">{{artist.last_modified_date}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Data Version</th>
|
||||||
|
<td colspan="2">{{artist.version}}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
{% for work in comic.get_artists() %}
|
{% for work in comic.get_artists() %}
|
||||||
<p>
|
<p>
|
||||||
{{work}}:
|
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||||
<ul>
|
<ul>
|
||||||
{% for artist in comic.get_artists()[work] %}
|
{% for artist in comic.get_artists()[work] %}
|
||||||
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||||
|
|||||||
@@ -10,9 +10,15 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<form class="d-flex" action="/comic/comics/">
|
||||||
|
<input class="form-control me-2" name="query" id="autocomplete" type="search" placeholder="Search" aria-label="Search">
|
||||||
|
Completed<input type="checkbox" name="completed" {% if request.query_params.get("completed")=="on" %}checked{% endif %} aria-label="Completed">
|
||||||
|
Order<input type="checkbox" name="order" {% if request.query_params.get("order")=="on" %}checked{% endif %} aria-label="Order">
|
||||||
|
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
<h1 class="display-5">Comics..</h1>
|
<h1 class="display-5">Comics..</h1>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
|
|||||||
@@ -50,12 +50,29 @@
|
|||||||
<a href="/comic/comics/{{issue.comic_id}}">{{issue.comic.title}}</a>
|
<a href="/comic/comics/{{issue.comic_id}}">{{issue.comic.title}}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if issue.volume %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Volume</th>
|
<th scope="row">Volume</th>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<a href="/comic/comics/{{issue.volume_id}}">{{issue.volume.name}}</a>
|
<a href="/comic/comics/{{issue.volume_id}}">{{issue.volume.name}}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Works</th>
|
||||||
|
<td colspan="2">
|
||||||
|
{% for work in issue.get_artists() %}
|
||||||
|
<p>
|
||||||
|
<a href="/comic/worktypes/{{work.id}}">{{work.name}}</a>
|
||||||
|
<ul>
|
||||||
|
{% for artist in issue.get_artists()[work] %}
|
||||||
|
<li><a href="/comic/artists/{{artist.id}}">{{artist.name}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Data Created</th>
|
<th scope="row">Data Created</th>
|
||||||
<td colspan="2">{{issue.created_date}}</td>
|
<td colspan="2">{{issue.created_date}}</td>
|
||||||
|
|||||||
@@ -10,7 +10,24 @@ router = APIRouter(include_in_schema=False, prefix="/comic")
|
|||||||
|
|
||||||
@router.get("/comics")
|
@router.get("/comics")
|
||||||
def get_comics(db: SessionDep, request: Request, msg: str | None = None):
|
def get_comics(db: SessionDep, request: Request, msg: str | None = None):
|
||||||
comics = db.query(Comic).all()
|
params = request.query_params
|
||||||
|
query = params.get("query")
|
||||||
|
filter = {}
|
||||||
|
completed = params.get('completed') == "on"
|
||||||
|
if completed:
|
||||||
|
filter['completed'] = True
|
||||||
|
order = params.get("order") == "on"
|
||||||
|
if order:
|
||||||
|
filter['current_order'] = True
|
||||||
|
if query is not None and len(query) > 0:
|
||||||
|
filter['title'] = query
|
||||||
|
if len(filter) > 0:
|
||||||
|
if "title" in filter:
|
||||||
|
comics = db.query(Comic).filter(Comic.title.ilike(f'%{query}%'))
|
||||||
|
else:
|
||||||
|
comics = db.query(Comic).filter_by(**filter).all()
|
||||||
|
else:
|
||||||
|
comics = db.query(Comic).all()
|
||||||
return templates.TemplateResponse("comic/comics.html", {"request": request, "msg": msg, "comics": comics})
|
return templates.TemplateResponse("comic/comics.html", {"request": request, "msg": msg, "comics": comics})
|
||||||
|
|
||||||
@router.get("/comics/{comic_id}")
|
@router.get("/comics/{comic_id}")
|
||||||
|
|||||||
@@ -70,6 +70,18 @@ public class Issue extends AbstractEntity {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFullTitle() {
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
stringBuilder.append(this.getComic().getTitle());
|
||||||
|
stringBuilder.append(" #");
|
||||||
|
stringBuilder.append(this.getIssueNumber());
|
||||||
|
if (this.title !=null && !this.title.isEmpty()) {
|
||||||
|
stringBuilder.append(": ");
|
||||||
|
stringBuilder.append(this.title);
|
||||||
|
}
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import com.vaadin.flow.component.textfield.TextField;
|
|||||||
|
|
||||||
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
||||||
import com.vaadin.flow.data.binder.Binder;
|
import com.vaadin.flow.data.binder.Binder;
|
||||||
import de.thpeetz.kontor.comics.data.*;
|
import de.thpeetz.kontor.comics.data.Comic;
|
||||||
|
import de.thpeetz.kontor.comics.data.Issue;
|
||||||
|
import de.thpeetz.kontor.comics.data.IssueWork;
|
||||||
|
import de.thpeetz.kontor.comics.data.Volume;
|
||||||
import de.thpeetz.kontor.common.views.YearMonthField;
|
import de.thpeetz.kontor.common.views.YearMonthField;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ import com.vaadin.flow.data.binder.BeanValidationBinder;
|
|||||||
import com.vaadin.flow.data.binder.Binder;
|
import com.vaadin.flow.data.binder.Binder;
|
||||||
|
|
||||||
import de.thpeetz.kontor.comics.data.Artist;
|
import de.thpeetz.kontor.comics.data.Artist;
|
||||||
import de.thpeetz.kontor.comics.data.Issue;
|
import de.thpeetz.kontor.comics.data.Comic;
|
||||||
import de.thpeetz.kontor.comics.data.IssueWork;
|
import de.thpeetz.kontor.comics.data.IssueWork;
|
||||||
import de.thpeetz.kontor.comics.data.Worktype;
|
import de.thpeetz.kontor.comics.data.Worktype;
|
||||||
|
import de.thpeetz.kontor.common.views.ComicIssueField;
|
||||||
|
|
||||||
public class IssueWorkForm extends FormLayout {
|
public class IssueWorkForm extends FormLayout {
|
||||||
ComboBox<Issue> issue = new ComboBox<>("Issue");
|
ComicIssueField issue = new ComicIssueField("Issue");
|
||||||
ComboBox<Artist> artist = new ComboBox<>("Artist");
|
ComboBox<Artist> artist = new ComboBox<>("Artist");
|
||||||
ComboBox<Worktype> workType = new ComboBox<>("Worktype");
|
ComboBox<Worktype> workType = new ComboBox<>("Worktype");
|
||||||
|
|
||||||
@@ -29,12 +30,13 @@ public class IssueWorkForm extends FormLayout {
|
|||||||
|
|
||||||
Binder<IssueWork> binder = new BeanValidationBinder<>(IssueWork.class);
|
Binder<IssueWork> binder = new BeanValidationBinder<>(IssueWork.class);
|
||||||
|
|
||||||
public IssueWorkForm(List<Issue> issues, List<Artist> artists, List<Worktype> workTypes) {
|
public IssueWorkForm(List<Comic> comics, List<Artist> artists, List<Worktype> workTypes) {
|
||||||
addClassName("issuework-form");
|
addClassName("issuework-form");
|
||||||
binder.bindInstanceFields(this);
|
binder.bindInstanceFields(this);
|
||||||
|
|
||||||
issue.setItems(issues);
|
issue.setComics(comics);
|
||||||
issue.setItemLabelGenerator(Issue::getIssueNumber);
|
//issue.setItems(issues);
|
||||||
|
//issue.setItemLabelGenerator(Issue::getIssueNumber);
|
||||||
artist.setItems(artists);
|
artist.setItems(artists);
|
||||||
artist.setItemLabelGenerator(Artist::getName);
|
artist.setItemLabelGenerator(Artist::getName);
|
||||||
workType.setItems(workTypes);
|
workType.setItems(workTypes);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package de.thpeetz.kontor.comics.views;
|
package de.thpeetz.kontor.comics.views;
|
||||||
|
|
||||||
|
import de.thpeetz.kontor.comics.data.Issue;
|
||||||
import de.thpeetz.kontor.comics.data.IssueWork;
|
import de.thpeetz.kontor.comics.data.IssueWork;
|
||||||
|
import lombok.Getter;
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
|
|
||||||
import com.vaadin.flow.component.Component;
|
import com.vaadin.flow.component.Component;
|
||||||
@@ -24,7 +26,9 @@ import jakarta.annotation.security.PermitAll;
|
|||||||
@PageTitle("IssueWork | Comics | Kontor")
|
@PageTitle("IssueWork | Comics | Kontor")
|
||||||
public class IssueWorkView extends VerticalLayout {
|
public class IssueWorkView extends VerticalLayout {
|
||||||
|
|
||||||
|
@Getter
|
||||||
Grid<IssueWork> grid = new Grid<>(IssueWork.class);
|
Grid<IssueWork> grid = new Grid<>(IssueWork.class);
|
||||||
|
@Getter
|
||||||
IssueWorkForm form;
|
IssueWorkForm form;
|
||||||
ComicService service;
|
ComicService service;
|
||||||
|
|
||||||
@@ -39,24 +43,16 @@ public class IssueWorkView extends VerticalLayout {
|
|||||||
updateList();
|
updateList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Grid<IssueWork> getGrid() {
|
|
||||||
return grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureGrid() {
|
private void configureGrid() {
|
||||||
grid.addClassName("issue-grid");
|
grid.addClassName("issue-grid");
|
||||||
grid.setSizeFull();
|
grid.setSizeFull();
|
||||||
grid.setColumns("issue.title", "artist.name", "workType.name");
|
grid.setColumns("issue.fullTitle", "artist.name", "workType.name");
|
||||||
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
grid.asSingleSelect().addValueChangeListener(event -> editIssueWork(event.getValue()));
|
grid.asSingleSelect().addValueChangeListener(event -> editIssueWork(event.getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IssueWorkForm getForm() {
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureForm() {
|
private void configureForm() {
|
||||||
form = new IssueWorkForm(service.findAllIssues(), service.findAllArtists(null),
|
form = new IssueWorkForm(service.findAllComics(null), service.findAllArtists(null),
|
||||||
service.findAllWorktypes(null));
|
service.findAllWorktypes(null));
|
||||||
form.setWidth("25em");
|
form.setWidth("25em");
|
||||||
form.setVisible(false);
|
form.setVisible(false);
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package de.thpeetz.kontor.common.views;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.Unit;
|
||||||
|
import com.vaadin.flow.component.customfield.CustomField;
|
||||||
|
import com.vaadin.flow.component.select.Select;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import de.thpeetz.kontor.comics.data.Comic;
|
||||||
|
import de.thpeetz.kontor.comics.data.Issue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class ComicIssueField extends CustomField<Issue> {
|
||||||
|
|
||||||
|
public final Select<Comic> comic = new Select<>();
|
||||||
|
public final Select<Issue> issue = new Select<>();
|
||||||
|
|
||||||
|
public ComicIssueField(String caption) {
|
||||||
|
comic.setEnabled(false);
|
||||||
|
comic.setWidth(9, Unit.EM);
|
||||||
|
comic.setItemLabelGenerator(Comic::getTitle);
|
||||||
|
comic.addValueChangeListener(e -> {
|
||||||
|
updateIssues();
|
||||||
|
});
|
||||||
|
issue.setEnabled(false);
|
||||||
|
issue.setWidth(9, Unit.EM);
|
||||||
|
issue.setItemLabelGenerator(Issue::getIssueNumber);
|
||||||
|
add(comic, issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Issue generateModelValue() {
|
||||||
|
log.info("ComicIssueField.generateModelValue: {}", issue.getValue());
|
||||||
|
return issue.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setPresentationValue(Issue newPresentationValue) {
|
||||||
|
log.info("ComicIssueField.setPresentationValue({})", newPresentationValue);
|
||||||
|
if (newPresentationValue == null) return;
|
||||||
|
comic.setValue(newPresentationValue.getComic());
|
||||||
|
log.info("setPresentationValue: set Comic select = {}", comic.getValue());
|
||||||
|
issue.setValue(newPresentationValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComics(List<Comic> comics) {
|
||||||
|
log.info("ComicIssueField.setComics");
|
||||||
|
comic.setItems(comics);
|
||||||
|
comic.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateIssues() {
|
||||||
|
log.info("ComicIssueField.updateIssues");
|
||||||
|
if (comic.getValue() == null) {
|
||||||
|
issue.setValue(null);
|
||||||
|
issue.setEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
issue.setItems(comic.getValue().getIssues());
|
||||||
|
issue.setEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user