webapp: jinja 2.8 doesn't support dotted assignments
[~helmut/crossqa.git] / webapp.py
1 #!/usr/bin/python3
2
3 import datetime
4 import lzma
5
6 import apt_pkg
7 apt_pkg.init()
8 version_compare = apt_pkg.version_compare
9
10 import flask
11 import flask_sqlalchemy
12 import jinja2
13 import sqlalchemy
14 import werkzeug
15
16 app = flask.Flask("crossqa")
17 app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///db'
18 db = flask_sqlalchemy.SQLAlchemy(app)
19
20 src_template = """<!DOCTYPE html>
21 <html>
22  <head>
23  </head>
24  <body>
25   <title>{{ sourcepackage|e }} - Debian cross build</title>
26   <h1>{{ sourcepackage|e }}</h1>
27   <h3>Cross satisfiability</h3>
28   <table>
29    <tr>
30     <th>state</th>
31     <th>architectures</th>
32    </tr>
33    {%- set okarchs = depresult.pop(None, None) -%}
34    {%- for reason, archs in depresult.items()|sort -%}
35     <tr>
36      <td>{{ reason|e }}</td>
37      <td>{{ archs|arch_format }}</td>
38     </tr>
39    {%- endfor -%}
40    {%- if okarchs -%}
41     <tr>
42      <td>ok</td>
43      <td>{{ okarchs|arch_format }}</td>
44     </tr>
45    {%- endif -%}
46   </table>
47   <h5>See also</h5>
48   <ul>
49    {%- if show_bootstrapdn -%}
50     <li>
51      <a href="https://bootstrap.debian.net/cross_all/{{ sourcepackage|e }}.html">bootstrap.debian.net</a>
52     </li>
53    {%- endif -%}
54    <li>
55     <a href="https://qa.debian.org/dose/debcheck/cross_unstable_main_amd64/">debcheck</a>
56    </li>
57   </ul>
58   {%- if builds -%}
59    <h3>Cross builds</h3>
60    <table>
61     <tr>
62      <th>started</th>
63      <th>version</th>
64      <th>architecture</th>
65      <th>result</th>
66     </tr>
67     {%- for build in builds|sort(attribute='starttime', reverse=true) -%}
68      <tr>
69       <td>
70        <span title="{{ build.starttime|sqltimestamp }}">
71         {{- build.starttime|sqltimestamp|formatts -}}
72        </span>
73       </td>
74       <td>{{ build.version|e }}</td>
75       <td>{{ build.architecture|e }}</td>
76       <td>
77        <a href="{{ url_for("show_log", filename=build.filename[:-3]) }}">
78         {{- "ok" if build.success else "failed" -}}
79        </a>
80        <a href="{{ url_for("show_log", filename=build.filename) }}">xz</a>
81       </td>
82      </tr>
83     {%- endfor -%}
84    </table>
85   {%- endif -%}
86  </body>
87 </html>
88 """
89
90 @app.template_filter("sqltimestamp")
91 def sqltimestamp_filter(s):
92     return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S.%f")
93
94 @app.template_filter("formatts")
95 def formatts_filter(ts):
96     assert isinstance(ts, datetime.datetime)
97     dt = datetime.datetime.utcnow() - ts
98     if dt < datetime.timedelta(seconds=1):
99         return "now"
100     if dt < datetime.timedelta(seconds=100):
101         return "%d s" % dt.seconds
102     if dt < datetime.timedelta(minutes=100):
103         return "%d m" % (dt.seconds // 60)
104     if dt < datetime.timedelta(days=1):
105         return "%d h" % (dt.seconds // (60 * 60))
106     return "%d d" % dt.days
107
108 @app.template_filter('arch_format')
109 @jinja2.contextfilter
110 def arch_format_filter(context, some_archs):
111     if context["architectures"] == some_archs:
112         return "any"
113     return ", ".join(sorted(some_archs))
114
115 def collect_depstate(conn, source):
116     version = None
117     depstate = None
118     query = sqlalchemy.text("""
119         SELECT version, architecture, satisfiable, reason
120             FROM depstate WHERE source = :source;""")
121     for row in conn.execute(query, source=source):
122         if version is None or version_compare(version, row.version) > 0:
123             version = row.version
124             depstate = {}
125         depstate[row.architecture] = None if row.satisfiable else row.reason
126     if version is None:
127         raise werkzeug.exceptions.NotFound()
128     depresult = {}
129     for arch, reason in depstate.items():
130         depresult.setdefault(reason, set()).add(arch)
131     return version, depresult
132
133 @app.route("/src/<source>")
134 def show_source(source):
135     context = dict(sourcepackage=source)
136     with db.engine.connect() as conn:
137         query = sqlalchemy.text("SELECT architecture FROM depcheck;")
138         context["architectures"] = set(row[0] for row in conn.execute(query))
139         context["version"], context["depresult"] = collect_depstate(conn,
140                                                                     source)
141         query = sqlalchemy.text("""
142             SELECT version, architecture, success, starttime, filename
143                 FROM builds WHERE source = :source;""")
144         context["builds"] = list(conn.execute(query, source=source))
145         context["show_bootstrapdn"] = \
146                 any(reason and not reason.startswith("skew ")
147                     for reason in context["depresult"].keys())
148     return flask.render_template_string(src_template, **context)
149
150 @app.route("/build/<path:filename>")
151 def show_log(filename):
152     if filename.endswith(".xz"):
153         return flask.send_from_directory("logs", filename,
154                                          mimetype="application/octet-stream")
155     filename += ".xz"
156     return flask.send_file(lzma.open(flask.safe_join("logs", filename), "rb"),
157                            mimetype="text/plain")