plom.client module

Plom Client

Plom client and supporting functions.

class plom.client.Chooser(Qapp)[source]
closeEvent(self, a0: QCloseEvent | None)[source]
getQuestion() int | None[source]

Return the integer question or None.

getv() int | None[source]

Return the integer version or None.

login() None[source]

Login to the server but don’t start any tasks yet.

Also update the UI with restricted questions and versions.

logout() None[source]

Logout if not already logged out.

Its safe to call this if you’re not logged in, don’t have a messenger etc.

partial_parse_address() None[source]

If address has a port number in it, extract and move to the port box.

If there’s a colon in the address (maybe user did not see port entry box or is pasting in a string), then try to extract a port number and put it into the entry box.

In some rare cases, we actively clear the port box, for example when the URL seems to have a path.

saveDetails() None[source]

Write the options to the config file.

setFontSize(n: int) None[source]

Adjust font size of user interface.

Parameters:

n – the desired font size in points.

setServer(s: str) None[source]

Set the server and port UI widgets from a string.

If port is missing, a default will be used. If we cannot parse the url, just leave it alone.

start_messenger_get_info(*, _legacy_username: str | None = None, verify_ssl: bool = True) None[source]

Get info from server, update UI with server version, check SSL.

Keyword Arguments:
  • _legacy_username – normally we don’t care who might eventually login, except in the legacy case and if it might be the manager.

  • verify_ssl – True by default but if False then don’t pop up dialogs about lacking SSL verification. Should not be used lightly! Currently we let users make this decision one login at a time.

Returns:

None, but modifies the state of the internal messenger instance variable. If that is not None, you can assume something reasonable happened.

class plom.client.IDClient(Qapp, tmpdir=None)[source]

Initialize the Identifier Client.

Parameters:
  • Qapp (QApplication) – Main client application

  • tmpdir (pathlib.Path/str/None) – a temporary directory for storing image files and other data. In principle can be shared with Marker although this may not be implemented. If None, we will make our own.

closeEvent(self, a0: QCloseEvent | None)[source]
enterID()[source]

Triggered when user hits return in the ID-lineedit.

For example, when they have entered a full student ID.

getClassList()[source]

Get the classlist from the server.

Here and throughout ‘snid’ means “student_id_and_name” as one string.

Returns nothing but modifies the state of self, adding three dicts to the class data:

snid_to_student_id snid_to_student_name student_id_to_snid

Raises:

PlomNoClasslist

getPredictions()[source]

Send request for prediction list to server.

identifyStudent(index, sid, sname, blank=False, no_id=False) bool | None[source]

Push identification of a paper to the server and misc UI table.

User ID’s the student of the current paper. Some care around whether or not the paper was ID’d previously. Not called directly - instead is called by “enterID” or “acceptPrediction” when user hits return on the line-edit.

Parameters:
  • index – an index into the UI table of the currently highlighted row.

  • sname (str) – The student name or special placeholder. - note that this should always be non-trivial string.

  • sid (str/None) – The student ID or None. - note that this is either ‘None’ (but only if blank or no_id is true), or should have passed the ‘is_valid_id’ test.

  • blank (bool) – the paper was blank: sid must be None and sname must be “Blank paper”.

  • no_id (bool) – paper is not blank but student did not fill-in the ID page(s). sid must be None and sname must be “No ID given”.

Returns:

True on success, False/None on failure.

Return type:

True/False/None

requestNext()[source]

Ask the server for an unID’d paper, get file, add to list, update image.

setCompleters()[source]

Set up the studentname + studentID line-edit completers.

Means that user can enter the first few numbers (or letters) and be prompted with little pop-up with list of possible completions.

setup(messenger)[source]

Performs setup procedure for the IDClient.

Parameters:

messenger (Messenger) – handles communication with server.

TODO: move all this into init?

shutDownError()[source]

Shuts down self due to error.

skipOnClick()[source]

Skip the current, moving to the next or loading a new one.

class plom.client.MarkerClient(Qapp, *, tmpdir=None)[source]

Setup for marking client and annotator.

Notes

TODO: should be a QMainWindow but at any rate not a Dialog TODO: should this be parented by the QApplication?

Signals:

my_shutdown_signal: called when TODO tags_changed_signal: emitted when tags might have changed, such

as after an explicit server refresh or some other event. The arguments are the task code (e.g., “0123g2”) and a list, possibly empty, of the current tags.

Initialize a new MarkerClient.

Parameters:

Qapp (QApplication) – Main client application

Keyword Arguments:

tmpdir (pathlib.Path/None) – a temporary directory for storing image files and other data. In principle can be shared with Identifier although this may not be implemented. If None, we will make our own. TODO: we don’t clean up this directory at all, which is reasonable enough if the caller made it, but a bit strange in the None case…

UIInitialization() None[source]

Startup procedure for the user interface.

Returns:

Modifies self.ui

Return type:

None

annotateTest()[source]

Grab current test from table, do checks, start annotator.

applyLastTimeOptions(lastTime: dict[str, Any]) None[source]

Applies all settings from previous client.

Parameters:
  • lastTime (dict) – information about settings, often from a

  • run. (config file such as from the last time the client was) –

Returns:

None

backgroundUploadFailed(task: str, errmsg: str, server_changed: bool, unexpected: bool) None[source]

An upload has failed, LOUDLY tell the user.

Parameters:
  • task – the task ID of the current test.

  • errmsg – the error message.

  • server_changed – True if this was something that changed server-side outside of our control.

  • unexpected – True if this wasn’t expected.

Returns:

None

backgroundUploadFinished(task: str, progress_info: dict[str, Any]) None[source]

An upload has finished, do appropriate UI updates.

Parameters:
  • task – the task ID of the upload.

  • progress_info – information about progress.

Returns:

None

background_download_failed(img_id)[source]

Callback when a background download has failed.

cacheLatexComments()[source]

Caches Latexed comments.

callbackAnnDoneCancel(task: str) None[source]

Called when annotator is done grading.

Parameters:

task – task name without the leading “q”.

Returns:

None

callbackAnnDoneClosing(task: str) None[source]

Called when annotator is done grading and is closing.

Parameters:

task – the task ID of the current test with no leading “q”.

Returns:

None

callbackAnnWantsUsToUpload(task, stuff) None[source]

Called when annotator wants to upload.

Parameters:
  • task (str) – the task ID of the current test.

  • stuff (list) – a list containing grade(int): grade given by marker. markingTime(int): total time spent marking. paperDir(dir): Working directory for the current task aname(str): annotated file name plomFileName(str): the name of the .plom file rubric(list[str]): the keys of the rubrics used integrity_check(str): the integrity_check string of the task.

Returns:

None

change_task_view(cbidx: int) None[source]

Update task list in response to combobox activation.

In some cases we reject the change and change the index ourselves.

choose_and_view_other() None[source]

Ask user to choose a paper number and question, then show images.

claim_task() None[source]

Try to claim the currently selected task for this user.

claim_task_and_trigger_downloads(task: str) None[source]

Claim a particular task for the current user and start image downloads.

Notes

Side effects: on success, updates the table of tasks by adding a new row (or modifying an existing one). But the new row is not automatically selected.

Returns:

None

Raises:
  • PlomTakenException

  • PlomVersionMismatchException

claim_task_interactive() None[source]

Ask user for paper number and then ask server for that paper.

If available, download stuff, add to list, update view.

closeEvent(self, a0: QCloseEvent | None)[source]
defer_task(*, advance_to_next: bool = True) None[source]

Mark task as “defer” - to be skipped until later.

You’ll still have to do it.

Keyword Arguments:

advance_to_next – whether to also advance to the next task (default).

download_task_list(*, username: str = '') bool[source]

Download and fill/update the task list.

Danger: there is quite a bit of subtly here about how to update tasks when we already have local cached data or when the local state might be mid-upload.

Keyword Arguments:

username – find tasks assigned to this user, or all tasks if omitted.

Returns:

True if the donload was successful, False if the server does not support this.

ensureAllDownloaded(new, old)[source]

Whenever the selection changes, ensure downloaders are either finished or running for each image.

We might need to restart downloaders if they have repeatedly failed. Even if we are still waiting, we can signal to the download the we have renewed interest in this particular download. TODO: for example. maybe we should send a higher priority? No: currently this also happens “in the background” b/c Marker selects the new row.

Parameters:
  • new (QItemSelection) – the newly selected cells.

  • old (QItemSelection) – the previously selected cells.

Returns:

None

getDataForAnnotator(task: str) tuple | None[source]

Start annotator on a particular task.

Parameters:

task – the task id. If original qXXXXgYY, then annotated version is GXXXXgYY (G=graded).

Returns:

A tuple of data or None.

getMorePapers(oldtgvID: str) tuple | None[source]

Loads more tests.

Parameters:

oldtgvID – the task code with no leading “q” for the previous thing marked.

Returns:

The data for the annotator or None as described in :method:`getDataForAnnotator`.

getOneRubricFromServer(key: int) dict[str, Any][source]

Get one rubric from server.

Parameters:

key – which rubric.

Returns:

Dictionary representation of the rubric..

Raises:

PlomNoRubric

getOtherRubricUsagesFromServer(key: str) list[int][source]

Get list of paper numbers using the given rubric.

Parameters:

key – the identifier of the rubric.

Returns:

List of paper numbers using the rubric.

getRubricsFromServer(question: int | None = None) list[dict[str, Any]][source]

Get list of rubrics from server.

Parameters:

question – pertaining to which question or None for all rubrics.

Returns:

A list of the dictionary objects.

getSolutionImage() Path | None[source]

Get the file from disc if it exists, else grab from server.

getTabStateFromServer()[source]

Download the state from the server.

get_current_task_id_or_none() str | None[source]

Give back the task id string of the currently highlighted row or None.

get_downloads_for_src_img_data(src_img_data: list[dict[str, Any]], trigger: bool = True) bool[source]

Make sure the images for some source image data are downloaded.

If an image is not yet downloaded, trigger the download again.

Parameters:

src_img_data (list) – list of dicts. Note we may modify this so pass a copy if you don’t want this! Specifically, the "filename" key is inserted or replaced with the path to the downloaded image or the placeholder image.

Keyword Arguments:

trigger (bool) – if True we trigger background jobs for any that have not been downloaded.

Returns:

True if all images have already been downloaded, False if at least one was not. In the False case, downloads have been triggered; wait; process events; then call back if you want.

Return type:

bool

get_file_for_previous_viewer(task: str) str | Path | None[source]

Get the annotation file for the given task.

Check to see if the local system already has the files for that task and if not grab them from the server. Then pass the annotation-image-file back to the caller.

get_files_for_previously_annotated(task: str) bool[source]

Loads the annotated image, the plom file, and the original source images.

TODO: maybe it could not aggressively download the src images: sometimes people just want to look at the annotated image.

Note that the local source image data will be replaced by data extracted from the Plom file.

Parameters:

task – the task for the image files to be loaded from. Takes the form “q1234g9” = test 1234 question 9

Returns:

True if the src_img_data, and the annotation files exist, False if not. User will have seen an error message if False is returned.

Raises:

Uses error dialogs; not currently expected to throw exceptions

get_src_img_data(task: str, *, cache: bool = False) list[dict[str, Any]][source]

Download the pagedate/src_img_data for a particular task.

Note this does not trigger downloads of the page images. For that, see :method:`get_downloads_for_src_img_data`.

Parameters:

task – which task to download the page data for.

Keyword Arguments:

cache – if this is one of the questions that we’re marking, also fill-in or update the client-side cache of pagedata. Caution: page-arranger might mess with the local copy, so for now be careful using this option. Default: False.

Returns:

The source image data.

Raises:

PlomConflict – no paper.

get_upload_queue_length()[source]

How long is the upload queue?

An overly long queue might be a sign of network troubles.

Returns:

The number of papers waiting to upload, possibly but not certainly including the current upload-in-progress. Value might also be approximate.

Return type:

int

latexAFragment(txt, *, quiet=False, cache_invalid=True, cache_invalid_tryagain=False)[source]

Run LaTeX on a fragment of text and return the file name of a PNG.

The files are cached for reuse if the same text is passed again.

Parameters:

txt (str) – the text to be Latexed.

Keyword Arguments:
  • quiet (bool) – if True, don’t popup dialogs on errors. Caution: this can result in a lot of API calls because users can keep requesting the same (bad) TeX from the server, e.g., by having bad TeX in a rubric.

  • cache_invalid (bool) – whether to cache invalid TeX. Useful to prevent repeated calls to render bad TeX but might prevent users from seeing (again) an error dialog that

  • cache_invalid_tryagain (bool) – if True then when we get a cache hit of None (corresponding to bad TeX) then we try to to render again.

Returns:

a path and filename to a .png of the rendered TeX. Or None if there was an error: callers will need to decide how to handle that, typically by displaying the raw code instead.

Return type:

pathlib.Path/str/None

loadMarkedList()[source]

Loads the list of previously marked papers into self.examModel.

Returns:

None

Deprecated: only called on legacy servers.

Note: this tries to update the history between sessions; we don’t try to do that on the new server, partially b/c the ordering seems fragile and I’m not sure its necessary.

manage_tags()[source]

Manage the tags of the current task.

manage_task_tags(task: str, parent: QWidget | None = None) None[source]

Manage the tags of a task.

Parameters:

task – A string like “q0003g2” for paper 3 question 2.

Keyword Arguments:

parent – Which window should be dialog’s parent? If None, then use self (which is Marker) but if other windows (such as Annotator or PageRearranger) are calling this and if so they should pass themselves: that way they would be the visual parents of this dialog.

Returns:

None

marker_has_reached_task_limit(*, use_cached: bool = True) bool[source]

Check whether a marker has reached their task limit if applicable.

Keyword Arguments:

use_cached – by default we use a cached value rather than connecting to the server to check.

Returns:

True if marker is in quota and they have reached their limit, otherwise False.

moveSelectionToTask(task)[source]

Update the selection in the list of papers.

moveToNextUnmarkedTest(task: str | None = None) bool[source]

Move the list to the next unmarked test, if possible.

Parameters:

task – the task number of the next unmarked test.

Returns:

True if move was successful, False if not, for any reason.

reassign_task()[source]

TODO: just a stub for now, no one is calling this.

refreshSolutionImage() Path | None[source]

Get solution image and save it to working dir.

refresh_server_data()[source]

Refresh various server data including the current task last from the server.

requestNext(*, update_select=True)[source]

Ask server for an unmarked paper, get file, add to list, update view.

Retry a few times in case two clients are asking for same.

Keyword Arguments:

update_select (bool) – default True, send False if you don’t want to adjust the visual selection.

Returns:

None

requestNextInBackgroundStart() None[source]

Requests the next TGV in the background.

Returns:

None

saveTabStateToServer(tab_state)[source]

Upload a tab state to the server.

setFilter()[source]

Sets a filter tag.

setup(messenger: Messenger, question_idx: int, version: int, lastTime: dict[str, Any]) None[source]

Performs setup procedure for MarkerClient.

TODO: move all this into init?

TODO: verify all lastTime Params, there are almost certainly some missing

Parameters:
  • messenger – handle communication with server.

  • question_idx – question index number, one based.

  • version – question version number

  • lastTime

    dict of settings. Containing:

    {
      "FOREGROUND"
      "KeyBinding"
    }
    

    and potentially others

Returns:

None

show_hide_technical()[source]

Toggle the technical panel in response to checking a button.

startTheAnnotator(initialData) None[source]

This fires up the annotation window for user annotation + marking.

Parameters:

initialData (list) – containing things documented elsewhere in :method:`getDataForAnnotator` and plom.client.annotator.Annotator.__init__().

Returns:

None

toggle_fail_mode()[source]

Toggle artificial failures simulatiing flaky networking in response to ticking a button.

updatePreviewImage(new, old)[source]

Updates the displayed image when the selection changes.

Parameters:
  • new (QItemSelection) – the newly selected cells.

  • old (QItemSelection) – the previously selected cells.

Returns:

None

updateProgress(*, info: dict[str, Any] | None = None) None[source]

Updates the progress bar and related display of progress information.

Keyword Arguments:

info – key-value pairs with information about progress overall, and for this user, including information about quota. If omitted, we’ll contact the server synchronously to get this info.

Returns:

None

May open dialogs in some circumstances.

view_other(paper_number: int, question_idx: int, *, _parent: QWidget | None = None, get_annotated: bool = True) None[source]

Shows a particular paper number and question.

Parameters:
  • paper_number – the paper number to be viewed.

  • question_idx – which question to be viewed.

Keyword Arguments:

get_annotated – whether to try to get the latest annotated image before falling back on the original scanned images. True by default.

Returns:

None

wait_for_bguploader(timeout=0)[source]

Wait for the uploader queue to empty.

Parameters:

timeout (int) – return early after approximately timeout seconds. If 0 then wait forever.

Returns:

True if it shutdown. False if we timed out.

Return type:

bool

class plom.client.annotator.Annotator(username: str, parentMarkerUI=None, initialData=None)[source]

The main annotation window for annotating group-images.

A subclass of QWidget

Initializes a new annotator window.

Parameters:
  • username (str) – username of Marker

  • parentMarkerUI (MarkerClient) – the parent of annotator UI.

  • initialData (dict) – as documented by the arguments to “load_new_question”

addImageMode()[source]

Opens a file dialog to choose an image.

Shows a message box if the image is too large, otherwise continues to image mode.

Notes

If the Image is greater than 200kb, will return an error.

Returns:

None

changeCBZoom(CBIndex: int) None[source]

Keeps zoom combo box at selected index.

Parameters:

CBIndex – the current zoom Combo Box Index

Returns:

None but modifies self.ui

change_annot_scale(scale=None)[source]

Change the scale of the annotations.

Parameters:

scale (float/None) – if None reset the scale to the default. If any floating point number, multiple the scale by that value.

change_annotation_colour()[source]

Ask user for a new colour for the annotations.

closeEvent(event: None | QCloseEvent) None[source]

Overrides the usual QWidget close event.

Deal with various cases of window trying to close.

Notes: These include:

  • User closes window via titlebar close icon (or alt-f4 or…)

  • User clicks “Cancel”

  • User clicks “Done”

Window close or Cancel are currently treated the same way: discard all annotations.

Parameters:

event – the event of the window closing.

Returns:

modifies many instance vars.

Return type:

None

close_current_question()[source]

Closes the current question, closes scene and clears instance vars.

Notes

As a result of this method, many instance variables will be None. Be cautious of how these variables will be handled in cases where they are None.

Returns:

Modifies self.

Return type:

None

close_current_scene() None[source]

Removes the current cene, saving some info in case we want to open a new one.

Returns:

Modifies self.

Return type:

None

createNewRubric(new_rubric) dict[str, Any][source]

Ask server to create a new rubric with data supplied.

getOneRubricFromServer(rid: int) dict[str, Any][source]

Request a latest rubric list for current question.

getOtherRubricUsagesFromServer(rid: int) list[int][source]

Request a list of paper numbers using the given rubric.

Parameters:

rid – the identifier of the rubric.

Returns:

List of paper numbers using the rubric, excluding the paper the annotator currently at.

getRubricsFromServer() list[dict[str, Any]][source]

Request a latest rubric list for current question.

getTabStateFromServer()[source]

Have Marker download the tab state from the server.

get_nonrubric_text_from_page() list[str][source]

Retrieves text (not in rubrics) from the scene.

Returns:

List of strings for text annotations not in a rubric.

handleRubric(rubric)[source]

Pass a rubric dict onward to the scene, if we have a scene.

Parameters:

rubric (dict) – we don’t care what’s in it: that’s for the scene and the rubric widget to agree on!

Returns:

Modifies self.scene

Return type:

None

isZoomFitHeight() bool[source]

Has the user selected “Fit Height”?

isZoomFitWidth() bool[source]

Has the user selected “Fit Width”?

is_dirty() bool[source]

Is the scene dirty?

Has the scene been annotated or changed this session? Re-opening a previous annotated scene does not dirty it, until changes are made. Changes could be made and then undone back to the clean state. The concept should be familiar to “file saved” in a text editor.

keyPopUp(*, tab_idx: int | None = None) None[source]

View help and keyboard shortcuts, eventually edit them.

Keyword Arg:
tab_idx: which tab to open in the help. If None

then we try to re-open on the same tab from last run.

keyToChangeRubric(keyNumber) None[source]

Translates a the numerical key into a selection of that visible row of the current rubric tab.

Returns:

modifies self.rubric_widget

Return type:

None

latexAFragment(*args, **kwargs)[source]

Latex a fragment of text.

loadCursors()[source]

Load custom cursors and set their hotspots.

Returns:

None

loadWindowSettings()[source]

Loads the window settings.

load_new_question(tgvID, question_label, version, max_version, testName, paperdir, saveName, maxMark, plomDict, integrity_check, src_img_data, tags: list[str])[source]

Loads new data into the window for marking.

Parameters:
  • tgvID (str) – Test-Group-Version ID code. For example, for Test #0027, group #13, version #2, we have t0027g13v2. TODO: currently only t0027g13, no version, despite name.

  • question_label (str) – The name of the question we are marking. This is generally used for display only as there is an integer for precise usage.

  • version (int) – which version are we working on?

  • max_version (int) – what is the largest version in this assessment?

  • testName (str) – Test Name

  • paperdir (dir) – Working directory for the current task

  • saveName (str/pathlib.Path) – file name (and dir, optionally) of the basename to save things (no .png/.jpg extension) If it does have an extension, it will be ignored.

  • maxMark (int) – maximum possible score for that test question

  • plomDict (dict) – a dictionary of annotation information. Contains sufficient information to recreate the annotation objects on the page if you go back to continue annotating a question.

  • integrity_check (str) – integrity check string

  • src_img_data (list[dict]) – image md5sums, filenames etc.

  • tags – currently has these tags. TODO: generally annotator doesn’t “do” tags itself, relying on Marker to know such things, but we need it to set attention for now. Maybe this could be refactored out to Marker later, after the wedding.

Returns:

Modifies many instance vars.

Return type:

None

modifyRubric(rid: int, updated_rubric: dict[str, Any]) dict[str, Any][source]

Ask server to modify an existing rubric with the new data supplied.

narrowLayout() None[source]

Changes view to narrow Layout style.

Returns:

None but modifies self.ui

new_or_permuted_image_data(src_img_data)[source]

We have permuted/added/removed underlying source images, tear done and build up again.

next_minor_tool(dir=1, always_move=False)[source]

Switch to current minor tool or advance to next minor tool.

Parameters:
  • dir (int) – +1 for next (default), -1 for previous.

  • always_move (bool) – the minor tools keep track of the last-used tool. Often, but not always, we want to switch back to the last-used tool. False by default.

next_rubric_or_reselect_rubric_tool()[source]

Changes the tool to rubric or pick the next rubric.

This allows the same key to switch back to rubrics (from say tick or delete tool) as is used to select the next rubric.

pause_to_process_events()[source]

Allow Qt’s event loop to process events.

Typically we call this if we’re in a loop of our own waiting for something to happen which can only occur if we

pickleIt() tuple[Path, Path][source]

Capture the annotated pages as a bitmap and a .plom file.

  1. Renders the current scene as a static bitmap.

  2. Retrieves current annotations in reverse chronological order.

  3. Adds various other metadata.

  4. Writes JSON into the .plom file.

Note: called “pickle” for historical reasons: it is neither a Python pickle nor a real-life pickle.

Returns:

two pathlib.Path, one for the rendered image and one for the .plom file.

Return type:

tuple

prev_minor_tool()[source]

Switch backward to the previous minor tool.

rearrangePages() None[source]

Rearranges pages in UI.

redo()[source]

Redoes the last action in the UI.

refreshDisplayedMark(score)[source]

Update the marklabel (and narrow one) with the current score - triggered by pagescene.

Returns:

None

refreshRubrics()[source]

Ask the rubric widget to refresh rubrics.

saveAndClose() None[source]

Save the current annotations, and then close.

Returns:

alters self.scene

Return type:

None

saveAndGetNext() None[source]

Saves the current annotations, and moves on to the next paper.

saveAnnotations() bool[source]

Try to save the annotations and signal Marker to upload them.

There are various checks and user interaction to be done.

Be careful of a score of 0 - when mark total or mark up. Be careful of max-score when marking down. In either case - get user to confirm the score before closing. Also confirm various “not enough feedback” cases.

Returns:

Return False if user cancels. Return True if we should move on (for example, to close the Annotator).

saveTabStateToServer(tab_state)[source]

Have Marker upload this tab state to the server.

saveWindowSettings()[source]

Saves current window settings and other state into the parent.

Returns:

modifies self.parentMarkerUI and self.scene

Return type:

None

setAllIcons() None[source]

Sets all icons for the ui Tool Buttons.

Returns:

None but modifies ui Tool Buttons.

setButtons()[source]

Connects buttons to their corresponding functions.

setIcon(toolButton, name, iconfile) None[source]

Sets a name and svg icon for a given QToolButton.

Parameters:
  • toolButton (QToolButton) – the ui Tool Button for a name and icon to be added to.

  • name (str) – a name defining toolButton.

  • iconfile (str) – filename of .svg, must be in the resource plom.client.icons.

Returns:

None but alters toolButton.

setMinorShortCuts()[source]

Setup non-editable shortcuts.

Each of these actions can be associated with multiple shortcut keys.

setToolMode(newMode, *, cursor=None, imagePath=None, rubric=None)[source]

Changes the current tool mode and cursor.

Parameters:

newMode (str) – "move", "rubric" etc.

Keyword Arguments:
  • imagePath – an argument for the “image” tool, used used only by the image tool.

  • rubric (dict[str, Any] | None) – if we’re changing to rubric, use this include the rubric.

  • cursor (str) – if None or omitted default cursors are used for each tool. If needed you could override this. (currently unused, semi-deprecated).

Notes

TODO: this does various other mucking around for legacy reasons: could probably still use some refactoring.

Returns:

None but modifies self.

setToolShortCuts()[source]

Set or change the shortcuts for the basic tool keys.

These are the shortcuts that are user-editable.

setViewAndScene(src_img_data)[source]

Makes a new scene (pagescene object) and connects it to the view (pageview object).

The pageview (which is a qgraphicsview) which is (mostly) a layer between the annotation widget and the graphics scene which actually stores all the graphics objects (the image, lines, boxes, text etc etc). The view allows us to zoom pan etc over image and its annotations.

Returns:

modifies self.scene from None to a pagescene object and connects it to a pageview object.

Return type:

None

setZoomComboBox() None[source]

Sets the combo box for the zoom method.

Returns:

None but modifies self.ui

tag_paper(task: str | None = None, dialog_parent: QWidget | None = None) None[source]

Open the tagging dialog on this task.

tags_changed(task: str, tags: list[str]) None[source]

React to possible tag change signals.

toggleTools() None[source]

Shows/Hides tools making more space to view the group-image.

Returns:

None but modifies self.ui.hideableBox

undo()[source]

Undoes the last action in the UI.

unpickleIt(plomData: dict[str, Any]) None[source]

Unpickles the page by calling scene.unpickleSceneItems and sets the page’s mark.

Parameters:

plomData – a dictionary containing the data for the pickled .plom file.

Returns:

None

update_annot_scale_menu_label()[source]

Update the menu which shows the current annotation scale.

update_attn_bar(*, tags: list[str] = [], show: bool = False) None[source]

Update the attention bar to show notifications to the user.

Keyword Arguments:
  • show – force showing the notification (default: False).

  • tags – a list of tags: if non-empty we’ll generate appropriate text and show the notification bar.

viewWholePaper() None[source]

Popup a dialog showing the entire paper.

TODO: this has significant duplication with RearrangePages.

view_other_paper(paper_number: int, *, _parent: QWidget | None = None) None[source]

Opens another dialog to view a paper.

Parameters:

paper_number – the paper number of the paper to be viewed.

Keyword:

_parent: override the default parent which is ourselves.

Returns:

None

wideLayout() None[source]

Changes view to Wide Layout style.

Returns:

None but modifies self.ui

zoomCBChanged() None[source]

Modifies the page view based on the selected zoom option.

Returns:

None but modifies self.ui

The background downloader downloads images using threads.

class plom.client.downloader.DownloadWorker(msgr, img_id, md5, target_name, *, basedir, simulate_failures=False)[source]
run(self)[source]
class plom.client.downloader.Downloader(basedir: str | Path, *, msgr: Messenger | None = None)[source]

Downloads and maintains a cache of images.

Downloader maintains a queue of downloads and emits signals whenever downloads succeed or fail.

Call download_in_background_thread() to enqueue an image for asynchronous download. Once enqueued, a download will be automatically retried several times, but to prevent endless data usage, it will give up after three tries. That is, clients cannot assume that something enqueued will inevitably be downloaded. Clients can check by TODO: document how to check if something is in the queue or/and or currently downloading.

Synchronous downloads can be performed with sync_download() and sync_downloads(). These images will also be cached.

TODO: document how to query the queue size. TODO: document how to query the size on disc.

The current queue can be cleared with clear_queue(). For shutting down the queue, see stop(). The Downlaoder keeps a clone of the messenger: if you logout (revoke the token) in another msgr while this is downloading, you’ll get a crash.

The Downloader will emit various signals. You can connect slots to these:

  • download_finished(img_id: int, md5sum: str, filename: str): emitted when a (background) download finishes. filename is newly-downloaded file.

  • download_failed(img_id: int): emitted when a (background) download fails. The job will be automatically restarted up to three times.

  • download_queue_changed(dict): the queue length changed (e.g., something enqueued or the queue is cleared). The signal argument is a dict of information about the queue.

Use enable_fail_mode() to artificially fail some download attempts and generally take longer. For debugging. Disable again with disable_fail_mode().

Initialize a new Downloader.

Parameters:

basedir – a directory for the image cache.

Keyword Arguments:

msgr – used for communication with a Plom server, or None and you can later call :method:`attach_messenger`. Note Messenger is not multithreaded and blocks using mutexes. Here we make our own private clone so caller can keep using their’s.

Returns:

None.

attach_messenger(msgr: Messenger) None[source]

Add/replace the current messenger.

clear_queue() None[source]

Cancel any enqueued (but not yet started) downloads.

Any existing downloads will continue, including their (up-to) three retries.

detach_messenger() None[source]

Stop our messenger and forget it (but do not logout).

download_in_background_thread(row: dict[str, Any], priority: bool = False, _is_retry: bool = False)[source]

Enqueue the downloading of particular row of the image database.

Parameters:

row (dict) – One image entry in the “page data”, has fields id, md5 and some others that are used to try to choose a reasonable local file name. Currently the local file name is chosen from the "server_path" key.

Keyword Arguments:
  • priority (bool) – high priority if user requested this (not a background download.

  • _is_retry (bool) – default False. If True, this signifies an automatic retry. Clients should probably not touch this.

Returns:

None

Does not start a new download if the Page Cache already has that image. It also tries to avoid enquing another request for the same image.

get_placeholder_path() str[source]

A static image that can be used as a placeholder while images are downloading.

Returns:

A real path on disc to the image, possibly a cached copy. TODO: might prefer a pathlib.Path but for now its a str.

Currently you may have to make a string (not an Path for example) b/c of some Qt limitations in the ExamModel and proxy stuff in Marker.

TODO: Issue #2357: better image or perhaps an animation?

has_messenger() bool[source]

Do we have a messenger?

stop(timeout: int = -1) bool[source]

Try to stop the downloader, after waiting for threads to clear.

Parameters:

timeout (int) – milliseconds seconds to wait before giving up. -1 to wait forever.

Returns:

True if all threads finished or False if timeout reached. In the False case, some cleanup tasks, such as removing files, probably did not occur. Feel free to try again.

Return type:

bool

sync_download(row: dict[str, Any]) dict[str, Any][source]

Given a row of “pagedata”, download synchronously and return edited row.

Parameters:

row – one row of the metadata for the set of all pages involved in a question. A list of dicts where each dict must have (at least) keys id, md5, server_path. TODO: sometimes we seem to accept md5sum instead: should fix that.

Returns:

the modified row. If the file was already downloaded, put its name into the filename key. If we had to download it we also put the filename into filename.

Return type:

dict

sync_downloads(pagedata: list[dict[str, Any]]) list[dict[str, Any]][source]

Given a block of “pagedata” download all images synchronously and return updated data.

Parameters:

pagedata – a list of dicts, each dict described in sync_download. Warning: we don’t make a copy: it will be modified (and returned).

Returns:

a list of dicts which consists of the updated input with filenames added/updated for each image.

Return type:

list

class plom.client.downloader.WorkerSignals[source]

Defines the signals available from a running worker thread.

Supported signals are:

finished:

No data

download_success:

(img_id (int), md5 (str), tempfile (str), targetfile (str)

download_fail:

(img_id (int), md5 (str), targetfile (str), err_stuff_tuple (tuple) where the tuple is (exctype, value, traceback.format_exc().

plom.manager module (legacy)

Plom server management tools for legacy servers.

class plom.manager.Manager(Qapp, *, server=None, user=None, password=None, manager_msgr=None)[source]

Plom server management and marking progress UI tool.

Start a new Plom Manager window.

Parameters:

Qapp (QApplication) – the application for whom we are opening a window.

Keyword Arguments:
  • manager_msgr (ManagerMessenger/None) – a connected ManagerMessenger. Note that the plain ‘ol Messenger will not work. By default or if None is passed, we’ll make the user login or use other kwargs.

  • server (str/None) – what server.

  • user (str/None) – credientials.

  • password (str/None) – credientials.

Returns:

None

closeEvent(self, a0: QCloseEvent | None)[source]
manage_task_tags(paper_num, question, parent=None)[source]

Manage the tags of a task.

Parameters:
  • paper_num (int/str) – paper number.

  • question (int/str) – question idex.

Keyword Arguments:

parent (Window/None) – Which window should be dialog’s parent? If None, then use self (which is Marker) but if other windows (such as Annotator or PageRearranger) are calling this and if so they should pass themselves: that way they would be the visual parents of this dialog.

Returns:

the current tags of paper/question. Note even if the dialog is cancelled, this will be updated (as someone else could’ve changed tags).

Return type:

list

refreshMRev()[source]

Refresh user list, tags, and any other server-dependent fields in marking review tab.

setFont(self, a0: QFont)[source]