def lookup_series(version=0, user=None): if version != 0: raise version_error(version) if request.method == "POST": raw_data = get_request_json() else: raw_data = get_request_args_json() series_id = _get_col("series_id", raw_data, "looking up a series") try: if user is None: sanitized = True else: sem = seminars_lookup(series_id) sanitized = not sem.user_can_edit(user=user) result = seminars_lookup(series_id, objects=False, sanitized=sanitized) except Exception as err: raise APIError({ "code": "lookup_error", "description": "an error occurred looking up the series", "error": str(err) }) talks = list( talks_search({"seminar_id": series_id}, sort=["start_time"], sanitized=sanitized, objects=False)) # tz = pytz.timezone(raw_data.get("timezone", result.get("timezone", "UTC"))) # TODO: adapt the times, support daterange, sort ans = {"code": "success", "properties": result, "talks": talks} callback = raw_data.get("callback", False) return str_jsonify(ans, callback)
def save_talk(version=0, user=None): if version != 0: raise APIError({"code": "invalid_version", "description": "Unknown API version: %s" % version}) raw_data = request.get_json() # Temporary measure while we rename seminar_id series_id = raw_data.pop("series_id", None) raw_data["seminar_id"] = series_id if series_id is None: raise APIError({"code": "unspecified_series_id", "description": "You must specify series_id when saving a talk"}) series = seminars_lookup(series_id) if series is None: raise APIError({"code": "no_series", "description": "The series %s does not exist (or is deleted)" % series_id}) else: # Make sure user has permission to edit if not series.user_can_edit(user): raise APIError({"code": "unauthorized_user", "description": "You do not have permission to edit %s." % series_id}, 401) # Temporary measure while we rename seminar_ctr series_ctr = raw_data.pop("series_ctr", None) raw_data["seminar_ctr"] = series_ctr if series_ctr is None: # Creating new talk talk = WebTalk(series_id, seminar=series, editing=True) else: talk = talks_lookup(series_id, series_ctr) if talk is None: raise APIError({"code": "no_talk", "description": "The talk %s/%s does not exist (or is deleted)" % (series_id, series_ctr)}) warnings = [] def warn(msg, *args): warnings.append(msg % args) data, errmsgs = process_save_talk(talk, raw_data, warn, format_error, format_input_error) if errmsgs: raise APIError({"code": "processing_error", "description": "Error in processing input", "errors": errmsgs}) else: new_version = WebTalk(talk.seminar_id, data=data) sanity_check_times(new_version.start_time, new_version.end_time) if talk.new or new_version != talk: # Talks saved by the API are not displayed until user approves new_version.display = False new_version.by_api = True new_version.save(user) else: raise APIError({"code": "no_changes", "description": "No changes detected"}) edittype = "created" if talk.new else "edited" if warnings: response = jsonify({"code": "warning", "description": "series successfully %s, but..." % edittype, "warnings": warnings}) else: response = jsonify({"code": "success", "description": "series successfully %s" % edittype}) return response
def can_edit_talk(seminar_id, seminar_ctr, token): """ INPUT: - ``seminar_id`` -- the identifier of the seminar - ``seminar_ctr`` -- an integer as a string, or the empty string (for new talk) - ``token`` -- a string (allows editing by speaker who might not have account) OUTPUT: - ``resp`` -- a response to return to the user (indicating an error) or None (editing allowed) - ``seminar`` -- a WebSeminar object, as returned by ``seminars_lookup(seminar_id)`` - ``talk`` -- a WebTalk object, as returned by ``talks_lookup(seminar_id, seminar_ctr)``, or ``None`` (if error or talk does not exist) """ new = not seminar_ctr if seminar_ctr: try: seminar_ctr = int(seminar_ctr) except ValueError: flash_error("Invalid talk id") return redirect(url_for("show_seminar", shortname=seminar_id), 301), None, None if token and seminar_ctr != "": talk = talks_lookup(seminar_id, seminar_ctr) if talk is None: flash_error("Talk does not exist") return redirect(url_for("show_seminar", shortname=seminar_id), 301), None, None elif token != talk.token: flash_error("Invalid token for editing talk") return redirect(url_for("show_talk", semid=seminar_id, talkid=seminar_ctr), 301), None, None seminar = seminars_lookup(seminar_id) else: resp, seminar = can_edit_seminar(seminar_id, new=False) if resp is not None: return resp, None, None if seminar.new: # TODO: This is where you might insert the ability to create a talk without first making a seminar flash_error("You must first create the seminar %s" % seminar_id) return redirect(url_for("edit_seminar", shortname=seminar_id), 301) if new: talk = WebTalk(seminar_id, seminar=seminar, editing=True) else: talk = WebTalk(seminar_id, seminar_ctr, seminar=seminar) return None, seminar, talk
def permdelete_seminar(shortname): seminar = seminars_lookup(shortname, include_deleted=True) if seminar is None: flash_error("Series %s already deleted permanently", shortname) return redirect(url_for(".index"), 302) if not current_user.is_subject_admin( seminar) and seminar.owner != current_user: flash_error("You do not have permission to delete seminar %s", shortname) return redirect(url_for(".index"), 302) if not seminar.deleted: flash_error("You must delete seminar %s first", shortname) return redirect(url_for(".edit_seminar", shortname=shortname), 302) else: db.seminars.delete({"shortname": shortname}) db.talks.delete({"seminar_id": shortname}) flash("Series %s permanently deleted" % shortname) return redirect(url_for(".index"), 302)
def lookup_talk(version=0, user=None): if version != 0: raise version_error(version) if request.method == "POST": raw_data = get_request_json() else: raw_data = get_request_args_json() series_id = _get_col("series_id", raw_data, "looking up a talk") series_ctr = _get_col("series_ctr", raw_data, "looking up a talk") if user is None: sanitized = True else: sem = seminars_lookup(series_id) sanitized = not sem.user_can_edit(user=user) result = talks_lookup(series_id, series_ctr, objects=False, sanitized=sanitized) ans = {"code": "success", "properties": result} callback = raw_data.get("callback", False) return str_jsonify(ans, callback)
def revive_seminar(shortname): seminar = seminars_lookup(shortname, include_deleted=True) if seminar is None: flash_error("Series %s was deleted permanently", shortname) return redirect(url_for(".index"), 302) if not current_user.is_subject_admin( seminar) and seminar.owner != current_user: flash_error("You do not have permission to revive seminar %s", shortname) return redirect(url_for(".index"), 302) if not seminar.deleted: flash_error("Series %s was not deleted, so cannot be revived", shortname) else: db.seminars.update({"shortname": shortname}, {"deleted": False}) db.talks.update({"seminar_id": shortname}, {"deleted": False}) flash( "Series %s revived. You should reset the organizers, and note that any users that were subscribed no longer are." % shortname) return redirect(url_for(".edit_seminar", shortname=shortname), 302)
def save_series(version=0, user=None): if version != 0: raise version_error(version) try: raw_data = get_request_json() except Exception: raw_data = None if not isinstance(raw_data, dict): raise APIError({ "code": "invalid_json", "description": "request must contain a json dictionary" }) # Temporary measure while we rename shortname series_id = raw_data.pop("series_id", None) raw_data["shortname"] = series_id if series_id is None: raise APIError({ "code": "unspecified_series_id", "description": "You must specify series_id when saving a series" }) series = seminars_lookup(series_id, include_deleted=True) if series is None: # Creating new series if not allowed_shortname( series_id) or len(series_id) < 3 or len(series_id) > 32: raise APIError({ "code": "invalid_series_id", "description": "The identifier must be 3 to 32 characters in length and can include only letters, numbers, hyphens and underscores." }) if "organizers" not in raw_data: raise APIError({ "code": "organizers_required", "description": "You must specify organizers when creating new series" }) update_organizers = True series = WebSeminar(series_id, data=None, editing=True, user=user) else: # Make sure user has permission to edit if not series.user_can_edit(user): raise APIError( { "code": "unauthorized_user", "description": "You do not have permission to edit %s." % series_id }, 401) if series.deleted: raise APIError({ "code": "norevive", "description": "You cannot revive a series through the API" }) if "organizers" in raw_data: raise APIError({ "code": "organizers_prohibited", "description": "You may not specify organizers when editing series" }) update_organizers = False # Check that there aren't extraneous keys (which might be misspelled) extra_keys = [ key for key in raw_data if key not in db.seminars.search_cols + ["slots", "organizers"] ] if extra_keys: raise APIError({ "code": "extra_keys", "description": "Unrecognized keys", "errors": extra_keys }) # Time slots/weekdays and organizers are handled differently by the processing code # We want to allow them to be unspecified (in which case we fall back on the current value) # and for an API call it's also more convenient to specify them using a list if "slots" in raw_data: slots = raw_data["slots"] else: slots = [ short_weekdays[day] + " " + slot for (day, slot) in zip(series.weekdays, series.time_slots) ] if not isinstance(slots, list) or len(slots) > MAX_SLOTS or not all( isinstance(slot, str) for slot in slots): raise APIError({ "code": "processing_error", "description": "Error in processing slots", "errors": [ "slots must be a list of strings of length at most %s" % MAX_SLOTS ] }) for i, slot in enumerate(slots): try: day, time = slot.split(None, 1) day = short_weekdays.index(day) except ValueError: raise APIError({ "code": "processing_error", "description": "Error in processing slots", "errors": [ "slots must be a three letter day-of-week followed by a time range after a space" ] }) raw_data["weekday%s" % i] = str(day) raw_data["time_slot%s" % i] = time for i in range(len(slots), MAX_SLOTS): raw_data["weekday%s" % i] = raw_data["time_slot%s" % i] = "" raw_data["num_slots"] = len(slots) if update_organizers: # We require specifying the organizers of a new seminar and don't allow updates, # so we don't need to get anything from the seminar object organizers = raw_data.get("organizers", []) fixed_cols = list(db.seminar_organizers.search_cols) i = fixed_cols.index("curator") fixed_cols[i] = "organizer" if not (isinstance(organizers, list) and len(organizers) <= MAX_ORGANIZERS and all( isinstance(OD, dict) and all(key in fixed_cols for key in OD) for OD in organizers)): raise APIError({ "code": "processing_error", "description": "Error in processing organizers", "errors": [ "organizers must be a list of dictionaries (max length %s) with keys %s" % (MAX_ORGANIZERS, ", ".join(fixed_cols)) ] }) for i, OD in enumerate(organizers): for col in db.seminar_organizers.search_cols: default = True if col == "display" else "" raw_data["org_%s%s" % (col, i)] = OD.get(col, default) # We store curator in the database but ask for organizer from the API raw_data["org_curator%s" % i] = not OD.get("organizer", True) warnings = [] def warn(msg, *args): warnings.append(msg % args) new_version, errmsgs = process_save_seminar(series, raw_data, warn, format_error, format_input_error, update_organizers, incremental_update=True, user=user) if new_version is None: raise APIError({ "code": "processing_error", "description": "Error in processing input", "errors": errmsgs }) if series.new or new_version != series: # Series saved by the API are not displayed until user approves new_version.display = False new_version.by_api = True new_version.save(user) else: raise APIError({ "code": "no_changes", "description": "No changes detected" }) if series.new: new_version.save_organizers() edittype = "created" if series.new else "edited" if warnings: response = jsonify({ "code": "warning", "description": "series successfully %s, but..." % edittype, "warnings": warnings }) else: response = jsonify({ "code": "success", "description": "series successfully %s" % edittype }) return response
def import_talks(csv_file): talks = [] ctr = {} with open(csv_file) as F: for i, line in enumerate(reader(F)): if i == 0: assert line == [ "Timestamp", "Title", "Speaker", "Speaker_inst", "Abstract", "Host", "Seminar", "Site", "In_Charge", "arXiv", "Date", "Start_Time", "End_Time", "Timezone", "Approved", ] continue ( timestamp, title, speaker, speaker_affiliation, abstract, host, seminar_id, site, in_charge, arXiv, date, start_time, end_time, timezone, approved, ) = line # Make sure seminar exists seminar = seminars_lookup(seminar_id) if not seminar: continue if seminar is None: print("Warning: seminar %s does not exist" % seminar_id) continue if seminar_id not in ctr: m = talks_max("seminar_ctr", {"seminar_id": seminar_id}) if m is None: m = -1 ctr[seminar_id] = m + 1 # This time zone info is specific to importing in early April # There is some broken data, where time zones were incrementing the minute. We reset them all to zero. tzdict = { -7: "America/Los_Angeles", -4: "America/New_York", -5: "America/Chicago", -3: "America/Buenos_Aires", 2: "Europe/Paris", 1: "Europe/London", } timezone = tzdict[int(timezone[4:7])] tz = pytz.timezone(timezone) date = parse(date, dayfirst=True).date() start_time = tz.localize( datetime.datetime.combine(date, parse(start_time).time())) end_time = tz.localize( datetime.datetime.combine(date, parse(end_time).time())) # Check to see if a talk at this time in the seminar already exists curtalk = talks_lucky({ "seminar_id": seminar_id, "speaker": speaker }) if curtalk is not None: print( "Talk with speaker %s already exists in seminar %s; continuing" % (speaker, seminar_id)) continue curtalk = talks_lucky({ "seminar_id": seminar_id, "start_time": start_time }) if curtalk is not None: print( "Talk at time %s (speaker %s) already exists in seminar %s; continuing" % (start_time.strftime("%a %b %d %-H:%M"), speaker, seminar_id)) continue topics = (arXiv.replace(" ", "").replace("Math.", "").replace( "math.", "").lower().split(",")) if not topics: topics = [] talks.append( dict( title=title, speaker=speaker, speaker_affiliation=speaker_affiliation, abstract=abstract, topics=topics, timezone=timezone, start_time=start_time, end_time=end_time, display=True, token="%016x" % random.randrange(16**16), online=True, live_link=seminar.live_link, room=seminar.room, access=seminar.access, comments=seminar.comments, seminar_id=seminar_id, seminar_ctr=ctr[seminar_id], )) ctr[seminar_id] += 1 return talks