1import os 2import posixpath 3 4import click 5from flask import Blueprint 6from flask import current_app 7from flask import g 8from flask import jsonify 9from flask import request 10 11from lektor._compat import iteritems 12from lektor._compat import itervalues 13from lektor.admin.utils import eventstream 14from lektor.environment import PRIMARY_ALT 15from lektor.publisher import publish 16from lektor.publisher import PublishError 17from lektor.utils import is_valid_id 18 19 20bp = Blueprint("api", __name__, url_prefix="/admin/api") 21 22 23def get_record_and_parent(path): 24 pad = g.admin_context.pad 25 record = pad.get(path) 26 if record is None: 27 parent = pad.get(posixpath.dirname(path)) 28 else: 29 parent = record.parent 30 return record, parent 31 32 33@bp.route("/pathinfo") 34def get_path_info(): 35 """Returns the path segment information for a record.""" 36 tree_item = g.admin_context.tree.get(request.args["path"]) 37 segments = [] 38 39 while tree_item is not None: 40 segments.append( 41 { 42 "id": tree_item.id, 43 "path": tree_item.path, 44 "label_i18n": tree_item.label_i18n, 45 "exists": tree_item.exists, 46 "can_have_children": tree_item.can_have_children, 47 } 48 ) 49 tree_item = tree_item.get_parent() 50 51 segments.reverse() 52 return jsonify(segments=segments) 53 54 55@bp.route("/recordinfo") 56def get_record_info(): 57 pad = g.admin_context.pad 58 request_path = request.args["path"] 59 tree_item = g.admin_context.tree.get(request_path) 60 children = [] 61 attachments = [] 62 alts = [] 63 64 for child in tree_item.iter_children(): 65 if child.is_attachment: 66 attachments.append(child) 67 else: 68 children.append(child) 69 70 primary_alt = pad.db.config.primary_alternative 71 if primary_alt is not None: 72 for alt in itervalues(tree_item.alts): 73 alt_cfg = pad.db.config.get_alternative(alt.id) 74 alts.append( 75 { 76 "alt": alt.id, 77 "is_primary": alt.id == PRIMARY_ALT, 78 "primary_overlay": alt.id == primary_alt, 79 "name_i18n": alt_cfg["name"], 80 "exists": alt.exists, 81 } 82 ) 83 84 child_order_by = pad.query(request_path).get_order_by() or [] 85 86 return jsonify( 87 id=tree_item.id, 88 path=tree_item.path, 89 label_i18n=tree_item.label_i18n, 90 exists=tree_item.exists, 91 is_attachment=tree_item.is_attachment, 92 attachments=[ 93 { 94 "id": x.id, 95 "path": x.path, 96 "type": x.attachment_type, 97 } 98 for x in attachments 99 ], 100 children=[ 101 { 102 "id": x.id, 103 "path": x.path, 104 "label": x.id, 105 "label_i18n": x.label_i18n, 106 "visible": x.is_visible, 107 } 108 for x in sorted( 109 children, key=lambda c: pad.get(c.path).get_sort_key(child_order_by) 110 ) 111 ], 112 alts=alts, 113 can_have_children=tree_item.can_have_children, 114 can_have_attachments=tree_item.can_have_attachments, 115 can_be_deleted=tree_item.can_be_deleted, 116 ) 117 118 119@bp.route("/previewinfo") 120def get_preview_info(): 121 alt = request.args.get("alt") or PRIMARY_ALT 122 record = g.admin_context.pad.get(request.args["path"], alt=alt) 123 if record is None: 124 return jsonify(exists=False, url=None, is_hidden=True) 125 return jsonify(exists=True, url=record.url_path, is_hidden=record.is_hidden) 126 127 128@bp.route("/find", methods=["POST"]) 129def find(): 130 alt = request.values.get("alt") or PRIMARY_ALT 131 lang = request.values.get("lang") or g.admin_context.info.ui_lang 132 q = request.values.get("q") 133 builder = current_app.lektor_info.get_builder() 134 return jsonify(results=builder.find_files(q, alt=alt, lang=lang)) 135 136 137@bp.route("/browsefs", methods=["POST"]) 138def browsefs(): 139 alt = request.values.get("alt") or PRIMARY_ALT 140 record = g.admin_context.pad.get(request.values["path"], alt=alt) 141 okay = False 142 if record is not None: 143 if record.is_attachment: 144 fn = record.attachment_filename 145 else: 146 fn = record.source_filename 147 if os.path.exists(fn): 148 click.launch(fn, locate=True) 149 okay = True 150 return jsonify(okay=okay) 151 152 153@bp.route("/matchurl") 154def match_url(): 155 record = g.admin_context.pad.resolve_url_path( 156 request.args["url_path"], alt_fallback=False 157 ) 158 if record is None: 159 return jsonify(exists=False, path=None, alt=None) 160 return jsonify(exists=True, path=record["_path"], alt=record["_alt"]) 161 162 163@bp.route("/rawrecord") 164def get_raw_record(): 165 alt = request.args.get("alt") or PRIMARY_ALT 166 ts = g.admin_context.tree.edit(request.args["path"], alt=alt) 167 return jsonify(ts.to_json()) 168 169 170@bp.route("/newrecord") 171def get_new_record_info(): 172 # XXX: convert to tree usage 173 pad = g.admin_context.pad 174 alt = request.args.get("alt") or PRIMARY_ALT 175 ts = g.admin_context.tree.edit(request.args["path"], alt=alt) 176 if ts.is_attachment: 177 can_have_children = False 178 elif ts.datamodel.child_config.replaced_with is not None: 179 can_have_children = False 180 else: 181 can_have_children = True 182 implied = ts.datamodel.child_config.model 183 184 def describe_model(model): 185 primary_field = None 186 if model.primary_field is not None: 187 f = model.field_map.get(model.primary_field) 188 if f is not None: 189 primary_field = f.to_json(pad) 190 return { 191 "id": model.id, 192 "name": model.name, 193 "name_i18n": model.name_i18n, 194 "primary_field": primary_field, 195 } 196 197 return jsonify( 198 { 199 "label": ts.record and ts.record.record_label or ts.id, 200 "can_have_children": can_have_children, 201 "implied_model": implied, 202 "available_models": dict( 203 (k, describe_model(v)) 204 for k, v in iteritems(pad.db.datamodels) 205 if not v.hidden or k == implied 206 ), 207 } 208 ) 209 210 211@bp.route("/newattachment") 212def get_new_attachment_info(): 213 ts = g.admin_context.tree.edit(request.args["path"]) 214 return jsonify( 215 { 216 "can_upload": ts.exists and not ts.is_attachment, 217 "label": ts.record and ts.record.record_label or ts.id, 218 } 219 ) 220 221 222@bp.route("/newattachment", methods=["POST"]) 223def upload_new_attachments(): 224 alt = request.values.get("alt") or PRIMARY_ALT 225 ts = g.admin_context.tree.edit(request.values["path"], alt=alt) 226 if not ts.exists or ts.is_attachment: 227 return jsonify({"bad_upload": True}) 228 229 buckets = [] 230 231 for file in request.files.getlist("file"): 232 buckets.append( 233 { 234 "original_filename": file.filename, 235 "stored_filename": ts.add_attachment(file.filename, file), 236 } 237 ) 238 239 return jsonify( 240 { 241 "bad_upload": False, 242 "path": request.form["path"], 243 "buckets": buckets, 244 } 245 ) 246 247 248@bp.route("/newrecord", methods=["POST"]) 249def add_new_record(): 250 values = request.get_json() 251 alt = values.get("alt") or PRIMARY_ALT 252 exists = False 253 254 if not is_valid_id(values["id"]): 255 return jsonify(valid_id=False, exists=False, path=None) 256 257 path = posixpath.join(values["path"], values["id"]) 258 259 ts = g.admin_context.tree.edit(path, datamodel=values.get("model"), alt=alt) 260 with ts: 261 if ts.exists: 262 exists = True 263 else: 264 ts.update(values.get("data") or {}) 265 266 return jsonify({"valid_id": True, "exists": exists, "path": path}) 267 268 269@bp.route("/deleterecord", methods=["POST"]) 270def delete_record(): 271 alt = request.values.get("alt") or PRIMARY_ALT 272 delete_master = request.values.get("delete_master") == "1" 273 if request.values["path"] != "/": 274 ts = g.admin_context.tree.edit(request.values["path"], alt=alt) 275 with ts: 276 ts.delete(delete_master=delete_master) 277 return jsonify(okay=True) 278 279 280@bp.route("/rawrecord", methods=["PUT"]) 281def update_raw_record(): 282 values = request.get_json() 283 data = values["data"] 284 alt = values.get("alt") or PRIMARY_ALT 285 ts = g.admin_context.tree.edit(values["path"], alt=alt) 286 with ts: 287 ts.update(data) 288 return jsonify(path=ts.path) 289 290 291@bp.route("/servers") 292def get_servers(): 293 db = g.admin_context.pad.db 294 config = db.env.load_config() 295 servers = config.get_servers(public=True) 296 return jsonify( 297 servers=sorted( 298 [x.to_json() for x in servers.values()], key=lambda x: x["name"].lower() 299 ) 300 ) 301 302 303@bp.route("/build", methods=["POST"]) 304def trigger_build(): 305 builder = current_app.lektor_info.get_builder() 306 builder.build_all() 307 builder.prune() 308 return jsonify(okay=True) 309 310 311@bp.route("/clean", methods=["POST"]) 312def trigger_clean(): 313 builder = current_app.lektor_info.get_builder() 314 builder.prune(all=True) 315 builder.touch_site_config() 316 return jsonify(okay=True) 317 318 319@bp.route("/publish") 320def publish_build(): 321 db = g.admin_context.pad.db 322 server = request.values["server"] 323 config = db.env.load_config() 324 server_info = config.get_server(server) 325 info = current_app.lektor_info 326 327 @eventstream 328 def generator(): 329 try: 330 event_iter = ( 331 publish( 332 info.env, 333 server_info.target, 334 info.output_path, 335 server_info=server_info, 336 ) 337 or () 338 ) 339 for event in event_iter: 340 yield {"msg": event} 341 except PublishError as e: 342 yield {"msg": "Error: %s" % e} 343 344 return generator() 345 346 347@bp.route("/ping") 348def ping(): 349 return jsonify(project_id=current_app.lektor_info.env.project.id, okay=True) 350