From dd31cc0165c9f8986f2ef0adfce27cd9a86a0de6 Mon Sep 17 00:00:00 2001 From: slarson Date: Fri, 31 Jul 2026 20:43:21 +0200 Subject: [PATCH 1/2] Fix the news feed: move to the Tumblr v2 API Tumblr put its legacy /api/read/json endpoint behind a bot challenge, so it now answers 403 for ordinary visitors. The news feed had been failing with "Unable to load news feed" on every page view. Move to the supported v2 API, which is auth-gated rather than bot-gated, using a read-only consumer key registered for the website alone - it can only read posts that are already public and cannot post, edit, or see drafts. Also fix the loading flakiness. loadFullNewsFeed() was being called twice on every page view, once from main.js and once from an inline script in news.html, so two overlapping requests raced to write the same list. The feed client now keeps at most one request per query in flight and caches results for fifteen minutes, so duplicate callers share a response instead of fighting over the DOM. Three rendering fixes came with the move to v2: species names are italicised at render time (Tumblr returns titles as plain text), a title repeated at the top of its own body is dropped, and titles derived from untitled photo posts are cut at a sentence boundary rather than a fixed width - taking care not to split "C. elegans" at its abbreviation. Co-Authored-By: Claude Opus 5 --- js/main.js | 481 ++++++++++++++++++++++++++++++++++++++++------------- news.html | 10 +- 2 files changed, 373 insertions(+), 118 deletions(-) diff --git a/js/main.js b/js/main.js index 13c579a..9956176 100644 --- a/js/main.js +++ b/js/main.js @@ -21,6 +21,8 @@ $(document).on('pjax:complete', function() { loadDonationControls(); } else if (loc === '/news.html') { loadFullNewsFeed(); + } else if (loc === '/events.html') { + loadEventsFeed(); } else if (loc === '/people.html') { $.getScript("https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.0/mustache.min.js", function() { @@ -93,6 +95,8 @@ $(window).on('load', function() { loadDonationControls(); } else if (loc === '/news.html') { loadFullNewsFeed(); + } else if (loc === '/events.html') { + loadEventsFeed(); } else if (loc === '/people.html') { $.getScript("https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.0/mustache.min.js", function() { @@ -131,148 +135,403 @@ function setNavigation() { } -function getTumblrPostTitle(post) { - if (post['regular-title']) return post['regular-title']; - if (post['link-text']) return post['link-text']; - if (post['quote-text']) return post['quote-text'].substring(0, 100); - if (post['photo-caption']) { - var tmp = document.createElement('div'); - tmp.innerHTML = post['photo-caption']; - return (tmp.textContent || tmp.innerText || '').substring(0, 100); +// ============================================================ +// Tumblr feed client - shared by index.html, news.html, events.html +// ============================================================ +// +// Tumblr put its legacy /api/read/json endpoint behind a bot challenge; it +// now answers 403 for ordinary visitors, which is what silently broke the +// news feed. Everything below uses the supported v2 API instead. +// +// TUMBLR_API_KEY is a public, read-only consumer key. It can only read posts +// that are already public on the blog - it cannot post, edit, or delete. + +var TUMBLR_BLOG = 'openworm.tumblr.com'; +var TUMBLR_API_KEY = 'Sr3W9pREVcJrnwiZb57tj8wRuCbP9d1npEynWqotXMoN5DDj9P'; +var TUMBLR_CACHE_MIN = 15; + +// Posts tagged with any of these show up on the events page. Both spellings +// are in use on the blog already, so accept both rather than silently +// dropping half the history. +var TUMBLR_EVENT_TAGS = ['event', 'events']; + +var _tumblrPending = {}; + +function _tumblrCacheGet(key) { + try { + var raw = window.sessionStorage.getItem(key); + if (!raw) return null; + var hit = JSON.parse(raw); + if ((Date.now() - hit.at) > TUMBLR_CACHE_MIN * 60 * 1000) return null; + return hit.posts; + } catch (e) { return null; } +} + +function _tumblrCacheSet(key, posts) { + try { + window.sessionStorage.setItem(key, JSON.stringify({ at: Date.now(), posts: posts })); + } catch (e) { /* private browsing / quota - caching is optional */ } +} + +// Fetch posts. opts: {tag, limit, done, fail} +// +// At most ONE request per distinct query is in flight at a time. Duplicate +// callers (window load racing pjax:complete, say) attach to the request that +// is already running instead of firing their own and fighting over the DOM. +function tumblrPosts(opts) { + opts = opts || {}; + var tag = opts.tag || ''; + var limit = opts.limit || 20; + var key = 'tumblr:' + tag + ':' + limit; + + var cached = _tumblrCacheGet(key); + if (cached) { if (opts.done) opts.done(cached); return; } + + if (_tumblrPending[key]) { _tumblrPending[key].push(opts); return; } + _tumblrPending[key] = [opts]; + + var settle = function(which, arg) { + var waiting = _tumblrPending[key] || []; + delete _tumblrPending[key]; + for (var i = 0; i < waiting.length; i++) { + if (waiting[i][which]) waiting[i][which](arg); + } + }; + + var params = { api_key: TUMBLR_API_KEY, limit: limit, filter: 'html' }; + if (tag) params.tag = tag; + + $.ajax({ + url: 'https://api.tumblr.com/v2/blog/' + TUMBLR_BLOG + '/posts', + data: params, + dataType: 'jsonp', + timeout: 15000 + }).done(function(data) { + var posts = (data && data.response && data.response.posts) || []; + _tumblrCacheSet(key, posts); + settle('done', posts); + }).fail(function(xhr, status) { + console.error('Tumblr fetch failed [' + key + ']:', status); + settle('fail', status); + }); +} + +// Fetch several tags at once and merge, newest first, de-duplicated by id. +function tumblrPostsForTags(tags, limit, done, fail) { + var collected = [], seen = {}, remaining = tags.length, anyOk = false; + + var finish = function() { + if (--remaining > 0) return; + if (!anyOk) { if (fail) fail('all tag queries failed'); return; } + collected.sort(function(a, b) { return b.timestamp - a.timestamp; }); + done(collected); + }; + + for (var i = 0; i < tags.length; i++) { + tumblrPosts({ + tag: tags[i], + limit: limit, + done: function(posts) { + anyOk = true; + for (var j = 0; j < posts.length; j++) { + if (!seen[posts[j].id]) { seen[posts[j].id] = true; collected.push(posts[j]); } + } + finish(); + }, + fail: finish + }); } - if (post['regular-body']) { - var tmp = document.createElement('div'); - tmp.innerHTML = post['regular-body']; - var text = tmp.textContent || tmp.innerText || ''; - return text.indexOf(':') !== -1 ? text.substring(0, text.indexOf(':')) : text; +} + +// ---- post field helpers (v2 shapes) ------------------------------------ + +function tumblrStripHtml(html) { + var tmp = document.createElement('div'); + tmp.innerHTML = html || ''; + return (tmp.textContent || tmp.innerText || '').replace(/\s+/g, ' ').trim(); +} + +// Tumblr returns post titles as plain text, so the italics that the rest of +// the site applies to species names are lost. Restore them at render time - +// the repo has explicit commits ("Italicise C. elegans") establishing this as +// house style, and the hand-written archive entries already follow it. +function italiciseSpecies(s) { + if (!s || s.indexOf('') !== -1 || s.indexOf('') !== -1) return s; + return s.replace(/\b(C\. elegans|Caenorhabditis elegans|Drosophila)\b/g, '$1'); +} + +// Photo and quote posts have no title, so one has to be derived from the +// caption. Cut at the first real sentence end rather than at a fixed width, +// otherwise the "title" is a mid-sentence slice of the body and the two read +// as duplicates. Abbreviations matter here: a naive split on ". " would cut +// "C. elegans" in half on a C. elegans blog. +function tumblrDeriveTitle(text) { + var line = (text || '').split('\n')[0].trim(); + if (line.length <= 60) return line; + var m = /[^\s]{2,}[.!?](\s|$)/.exec(line); + if (m && m.index + m[0].length >= 20 && m.index + m[0].length <= 130) { + return line.substring(0, m.index + m[0].trimEnd().length); + } + return line.substring(0, 120); +} + +function tumblrPostTitle(post) { + if (post.title) return post.title; + if (post.summary) return tumblrDeriveTitle(post.summary); + var text = tumblrStripHtml(post.body || post.caption || post.description || post.text); + if (!text) return 'Untitled post'; + return text.indexOf(':') !== -1 && text.indexOf(':') < 80 + ? text.substring(0, text.indexOf(':')) + : text.substring(0, 120); +} + +function tumblrPostBody(post) { + var html = ''; + if (post.type === 'photo' && post.photos && post.photos.length) { + var src = post.photos[0].original_size && post.photos[0].original_size.url; + if (src) html += ''; } - return 'Untitled post'; + html += post.body || post.caption || post.description || ''; + if (!html && post.text) { + html = '
' + post.text + '
' + (post.source || ''); + } + return html; } +// NPF posts carry their title as a heading block inside the body, so a naive +// render prints the title twice - once as the item heading, once at the top of +// the text. Drop the leading copy when it duplicates the heading we already +// rendered. +function stripLeadingTitle(html, title) { + if (!html || !title) return html; + var plain = tumblrStripHtml(title); + if (!plain) return html; + + var el = document.createElement('div'); + el.innerHTML = html; + + // Compare decoded text, not raw HTML: a title containing "&" appears as + // "&" in the markup, so string matching on the source silently fails. + // Skip leading media (photo posts open with an ) to reach the caption. + for (var i = 0; i < el.children.length; i++) { + var node = el.children[i]; + var txt = tumblrStripHtml(node.innerHTML); + if (!txt) continue; // img, spacer, empty node + if (txt === plain) { + node.parentNode.removeChild(node); + return el.innerHTML; + } + if (txt.indexOf(plain) === 0) { // title runs on into the body + var rest = txt.slice(plain.length).replace(/^[\s.,:;!?-]+/, ''); + node.textContent = rest; + return el.innerHTML; + } + break; // first real text isn't the title + } + return el.innerHTML; +} + +function tumblrFormatDate(d) { + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); +} + +// ---- event date convention --------------------------------------------- +// +// A post's publication date is when the event was ANNOUNCED, which is not +// the same thing as when the event happens - and for anything upcoming they +// point in opposite directions. So an event post may carry a date tag: +// +// date:2026-09-15 single day +// date:2026-01-29..2026-01-30 a range, rendered "29-30" +// +// With no date tag we fall back to the post's own date, which is the right +// answer for the historical posts that predate this convention. +function tumblrEventDate(post) { + // Date.UTC silently rolls impossible values over - Date.UTC(2026, 12, 99) + // is April 2027, not an error - so a typo in a tag would quietly produce a + // confidently wrong date. Build the date, then check it reads back the way + // it was written; anything that does not is treated as not a date tag. + var toUTC = function(y, mo, d) { + var dt = new Date(Date.UTC(y, mo - 1, d)); + if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d) return null; + return dt; + }; + + var tags = post.tags || []; + var re = /^(?:date:)?(\d{4})-(\d{2})-(\d{2})(?:\.\.(\d{4})-(\d{2})-(\d{2}))?$/; + for (var i = 0; i < tags.length; i++) { + var m = re.exec($.trim(tags[i])); + if (!m) continue; + var start = toUTC(+m[1], +m[2], +m[3]); + if (!start) continue; + var end = start; + if (m[4]) { + end = toUTC(+m[4], +m[5], +m[6]); + if (!end || end < start) end = start; + } + return { start: start, end: end, explicit: true }; + } + var posted = new Date(post.timestamp * 1000); + return { start: posted, end: posted, explicit: false }; +} + +// ---- home page: short news list ---------------------------------------- + function refreshNews() { - // Use Tumblr v1 JSONP API (no CORS needed) - $.ajax({ - url: 'https://openworm.tumblr.com/api/read/json', - data: { num: 6 }, - dataType: 'jsonp', - timeout: 30000, - success: function(data) { - var posts = data.posts || []; + tumblrPosts({ + limit: 6, + done: function(posts) { var html = ''; - for (var i = 0; i < posts.length; i++) { var post = posts[i]; - var title = getTumblrPostTitle(post); - var link = post['url-with-slug'] || post['url']; - var pubDate = new Date(post['unix-timestamp'] * 1000); - var dateStr = pubDate.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); - html += '
  • '; - html += '' + title + ''; - html += ' (' + dateStr + ')'; + html += '' + italiciseSpecies(tumblrPostTitle(post)) + ''; + html += ' (' + tumblrFormatDate(new Date(post.timestamp * 1000)) + ')'; html += '
  • '; } - $("#news-feed").html(html); + $('#news-feed').html(html); }, - error: function(err) { - console.error('Error loading news feed:', err); - $("#news-feed").html('
  • Unable to load news feed.
  • '); + fail: function() { + $('#news-feed').html('
  • Unable to load the news feed right now. ' + + 'Read it on Tumblr »
  • '); } }); } -function getTumblrPostBody(post) { - if (post['regular-body']) return post['regular-body']; - if (post['photo-caption']) return post['photo-caption']; - if (post['link-description']) return post['link-description']; - if (post['quote-text']) return '
    ' + post['quote-text'] + '
    ' + (post['quote-source'] || ''); - return ''; -} +// ---- news.html: full feed with sidebar --------------------------------- function loadFullNewsFeed() { - // Load full news feed with sidebar navigation for news.html page - console.log('Loading full news feed with sidebar...'); - - $.ajax({ - url: 'https://openworm.tumblr.com/api/read/json', - data: { num: 25 }, - dataType: 'jsonp', - timeout: 30000, - success: function(data) { - console.log('Feed loaded via JSONP'); - - var posts = data.posts || []; - console.log('Found ' + posts.length + ' posts'); - + tumblrPosts({ + limit: 25, + done: function(posts) { var mainHtml = ''; - var navHtml = ''; + var navHtml = ''; for (var i = 0; i < posts.length; i++) { - var post = posts[i]; - var title = getTumblrPostTitle(post); - var link = post['url-with-slug'] || post['url']; - var pubDate = new Date(post['unix-timestamp'] * 1000); - var dateStr = pubDate.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); - - var description = getTumblrPostBody(post); - - // Create anchor ID from index - var anchorId = 'news-' + i; - - // Add to sidebar nav (if nav element exists) - navHtml += '
  • ' + dateStr + '
  • '; - - // Add to main content - var borderStyle = (i < posts.length - 1) ? 'border-bottom: 1px solid #eee;' : ''; - mainHtml += '
  • '; - mainHtml += '

    ' + title + '

    '; + var post = posts[i]; + var dateStr = tumblrFormatDate(new Date(post.timestamp * 1000)); + var anchor = 'news-' + i; + + navHtml += '
  • ' + dateStr + '
  • '; + + var border = (i < posts.length - 1) ? 'border-bottom: 1px solid #eee;' : ''; + mainHtml += '
  • '; + mainHtml += '

    ' + italiciseSpecies(tumblrPostTitle(post)) + '

    '; mainHtml += '

    ' + dateStr + '

    '; - mainHtml += '
    ' + description + '
    '; + mainHtml += '
    ' + stripLeadingTitle(tumblrPostBody(post), tumblrPostTitle(post)) + '
    '; mainHtml += '
  • '; } - $("#news-feed-full").html(mainHtml); - - // Update sidebar nav if it exists - if ($("#news-nav").length) { - $("#news-nav").html(navHtml); - } - - console.log('Rendered ' + posts.length + ' items'); - - // Make images responsive - $("#news-feed-full img").css({ - "max-width": "100%", - "height": "auto", - "margin": "15px 0", - "display": "block" + $('#news-feed-full').html(mainHtml); + if ($('#news-nav').length) $('#news-nav').html(navHtml); + + $('#news-feed-full img').css({ + 'max-width': '100%', 'height': 'auto', 'margin': '15px 0', 'display': 'block' }); }, - error: function(xhr, status, err) { - console.error('Error loading full feed - Status:', status, 'Error:', err); - - var errorMsg = 'Unable to load news feed. '; - if (status === 'timeout') { - errorMsg += 'Request timed out.'; - } else { - errorMsg += 'Error: ' + status; + fail: function(status) { + var msg = (status === 'timeout') ? 'The request timed out.' : 'The feed could not be reached.'; + $('#news-feed-full').html('
  • ' + + msg + ' Read the blog directly »
  • '); + if ($('#news-nav').length) { + $('#news-nav').html('' + + '
  • View on Tumblr
  • '); } + } + }); +} + +// ---- events.html: events drawn from tagged blog posts ------------------- + +var MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +// "Jun 22, 2021" for a single day, "Jan 29-30, 2018" for a range - matching +// how the hand-written archive entries below the feed are written. +function tumblrFormatEventDate(when) { + var s = when.start, e = when.end; + var out = MONTH_ABBR[s.getUTCMonth()] + ' ' + s.getUTCDate(); + if (e.getTime() !== s.getTime()) { + out += (e.getUTCMonth() === s.getUTCMonth()) + ? '\u2013' + e.getUTCDate() + : '\u2013' + MONTH_ABBR[e.getUTCMonth()] + ' ' + e.getUTCDate(); + } + return out + ', ' + s.getUTCFullYear(); +} + +// Render one post in the same shape as a news item and as the archived +// events below it, so the whole page reads as one list. +function renderEventItem(post, anchor, isLast) { + var dateStr = tumblrFormatEventDate(tumblrEventDate(post)); + var border = isLast ? '' : 'border-bottom: 1px solid #eee;'; + + var html = '
  • '; + html += '

    ' + italiciseSpecies(tumblrPostTitle(post)) + '

    '; + html += '

    ' + dateStr + '

    '; - $("#news-feed-full").html('
  • ' + errorMsg + ' View blog directly »
  • '); - - // Update sidebar nav with fallback if it exists - if ($("#news-nav").length) { - $("#news-nav").html('
  • View on Tumblr
  • '); + var body = tumblrStripHtml(stripLeadingTitle(tumblrPostBody(post), tumblrPostTitle(post))); + if (body.length > 320) body = body.substring(0, 317).replace(/\s+\S*$/, '') + '...'; + if (body) html += '
    ' + body + '
    '; + + html += '

    ' + + ' Read the post

    '; + html += ''; + return html; +} + +function _eventSectionHeading(text) { + return '
  • ' + text + '

  • '; +} + +function loadEventsFeed() { + var $feed = $('#events-feed'); + if (!$feed.length) return; + + tumblrPostsForTags(TUMBLR_EVENT_TAGS, 50, function(posts) { + // "Today" at UTC midnight, so an event happening today still counts + // as upcoming rather than dropping into the past the morning of. + var now = new Date(); + var today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()); + + var upcoming = [], past = []; + for (var i = 0; i < posts.length; i++) { + (tumblrEventDate(posts[i]).end.getTime() >= today ? upcoming : past).push(posts[i]); + } + // Soonest first for what is ahead; most recent first for what is behind. + upcoming.sort(function(a, b) { return tumblrEventDate(a).start - tumblrEventDate(b).start; }); + past.sort(function(a, b) { return tumblrEventDate(b).start - tumblrEventDate(a).start; }); + + var html = '', navHtml = '', n = 0; + var addGroup = function(label, list) { + if (!list.length) return; + html += _eventSectionHeading(label); + for (var j = 0; j < list.length; j++) { + var anchor = 'event-' + (n++); + var last = (list === past) && (j === list.length - 1); + html += renderEventItem(list[j], anchor, last); + navHtml += '
  • ' + + tumblrFormatEventDate(tumblrEventDate(list[j])) + '
  • '; } + }; + addGroup('Coming up', upcoming); + addGroup('Recently', past); + + if (!html) { + html = '
  • No events posted to the blog yet — earlier events are listed below.
  • '; } + $feed.html(html); + $('#events-nav-loading').replaceWith(navHtml || + '
  • ' + + ' On Tumblr
  • '); + }, function() { + $feed.html('
  • ' + + 'Could not reach the blog just now. ' + + 'See events on Tumblr »
    ' + + 'Earlier events are listed below.
  • '); + $('#events-nav-loading').replaceWith( + '
  • ' + + ' On Tumblr
  • '); }); } diff --git a/news.html b/news.html index 20f67ca..3758dd7 100644 --- a/news.html +++ b/news.html @@ -101,13 +101,9 @@

    Latest News

    - + From d83ff573150ae83ba1153feac385d6def8bb328d Mon Sep 17 00:00:00 2001 From: slarson Date: Fri, 31 Jul 2026 20:43:34 +0200 Subject: [PATCH 2/2] Draw the events page from the blog The events page was hand-maintained and its newest entry was July 2025. It now renders from blog posts tagged "event" (or "events" - both spellings are in use), so adding an event means writing a post rather than editing HTML. Because a post's publication date is when an event was announced rather than when it happened, a post may also carry a "date:2026-09-15" tag, or "date:2026-01-29..2026-01-30" for something spanning several days. Events dated today or later appear under "Coming up", the rest under "Recently". Malformed dates fall back to the post date instead of inventing one. The page also had a visual identity of its own that did not match anything else on the site. It now uses the same layout as the news page - sidebar index, linked headings, muted dates - so the two read as one site. Padraig's recent additions and italics fixes are carried over intact. All twenty previously hand-written events now exist as backdated posts, so listing them here as well would show each one twice. The static markup is kept in this file, commented out, as a fallback: if the blog is ever lost the full 2011-2025 history is one uncomment away. The event tagging convention is documented in TUMBLR_MIGRATION_README.md. Co-Authored-By: Claude Opus 5 --- TUMBLR_MIGRATION_README.md | 43 +++ events.html | 624 +++++++++++++------------------------ 2 files changed, 264 insertions(+), 403 deletions(-) diff --git a/TUMBLR_MIGRATION_README.md b/TUMBLR_MIGRATION_README.md index 22565f7..ca9a917 100644 --- a/TUMBLR_MIGRATION_README.md +++ b/TUMBLR_MIGRATION_README.md @@ -288,3 +288,46 @@ The migration script creates 19 posts = 19 API calls, well within limits. **Last Updated:** January 31, 2026 **Maintainer:** OpenWorm Team + +--- + +## Posting an Event + +The website's [Events page](https://openworm.org/events.html) is drawn live from this +blog. There is nothing to edit on the website — post to Tumblr and the page updates. + +### Tags + +| Tag | Effect | +|-----|--------| +| `event` **or** `events` | Required. Puts the post on the Events page. Both spellings work; older posts use both. | +| `date:2026-09-15` | Optional. The date the event **happens**. | +| `date:2026-01-29..2026-01-30` | Optional. A multi-day event, rendered as "29-30 Jan". | + +A bare `2026-09-15` (no `date:` prefix) works too. + +### Why the date tag matters + +Without it the page falls back to the post's publication date — which is when the event +was *announced*, not when it happens. For anything upcoming those point in opposite +directions, and the event lands in the wrong section. + +A real example already on the blog: *"Join us in London! / November 5-6"* was posted on +**31 October 2014**, so it renders under 31 Oct. Tagging it `date:2014-11-05..2014-11-06` +would fix it. + +Malformed dates (`2026-13-99`, `2026-02-30`) are ignored and the post date is used +instead, so a typo degrades quietly rather than inventing a date. + +### Sections + +Events dated today or later appear under **Coming up**, soonest first. Everything else +appears under **Recently**, most recent first. Events predating the blog are kept as a +static **Earlier events** list in `events.html` — that list is hand-maintained and is +not affected by anything here. + +### Title and description + +The post title becomes the event title; the opening of the post body becomes the +description, trimmed to fit the card. Lead with the essentials — a post that opens with +"UPDATE 3 (2/11/17):" will show exactly that. diff --git a/events.html b/events.html index f7623d6..37583db 100644 --- a/events.html +++ b/events.html @@ -1,408 +1,226 @@ - - - OpenWorm Events - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - -
    -
    -

    Events

    - -

    - Where we have been and where to meet us next! -

    -
    -
    - -
    -
    -
    -
    -              -
    - -
    -
    -
    -
      - - -
    • - -
      -

      OpenWorm @ 2025 International Worm Meeting

      -

      The OpenWorm team were present at the International Worm Meeting at UC Davis in July 2025.

      - -
      -
    • - -
    • - -
      -

      OpenWorm @ C. elegans 2023 meeting

      -

      The OpenWorm team were present at the main C. elegans global scientific conference in Glasgow in June 2023.

      - -
      -
    • - -
    • - -
      -

      OpenWorm poster at C. elegans meeting

      -

      Online presentation of: The OpenWorm Project: progress update, available resources and future plans

      - -
      -
    • - -
    • - -
      -

      Open House @ Sainsbury Wellcome Centre

      -

      London, UK

      - -
      -
    • - - -
    • - -
      -

      Connectome to behaviour: modelling C. elegans at cellular resolution

      -

      Royal Society Discussion meeting, London, UK

      - -
      -
    • - -
    • - -
      -

      TEDxBangalore

      -

      Speaker: Stephen Larson, Bangalore, India

      - -
      -
    • - -
    • - -
      -

      Open Collaboration in Computational Neuroscience workshop

      -

      Neuroinformatics 2014, Leiden, The Netherlands

      - -
      -
    • - -
    • - -
      -

      Open Source Brain Workshop 2014

      -

      Alghero, Sardinia, Italy

      - -
      -
    • - -
    • - -
      -

      Neuroinformatics 2013

      -

      Karolinska Institutet, Stockholm, Sweden

      - -
      -
    • - -
    • - -
      -

      Computational Neuroscience Meeting 2013

      -

      Université Paris Descartes, Paris, France

      - -
      -
    • - -
    • - -
      -

      OpenWorm Paris Meet-up

      -

      Falstaff, Paris, France

      -
      -
    • - -
    • - -
      -

      Guest Lecture: Introduction to OpenWorm

      -

      Speaker: Matteo Cantarelli. Host: Daniele Giusto - Faculty of Electrical Engineering, University Of Cagliari, Italy

      -
      -
    • - -
    • - -
      -

      Open Source Brain Kick-Off meeting

      -

      Alghero, Sardinia, Italy

      - -
      -
    • - -
    • - -
      -

      OpenWorm Cambridge Meet-up

      -

      The Eagle, Cambridge, UK

      -
      -
    • - -
    • - -
      -

      OpenWorm London Meet-up

      -

      Queens Head & Artichoke, London, UK

      -
      -
    • - -
    • - -
      -

      Modeling C. elegans: The OpenWorm Project

      -

      Speaker: Mike Vella, Cambridge, UK

      - -
      -
    • - -
    • - -
      -

      NeuroInformatics 2012

      -

      Boston, USA

      - -
      -
    • - -
    • - -
      -

      Convergence in Computational Neuroscience 2012

      -

      Informatics Forum, Edinburgh, UK

      - -
      -
    • - -
    • - -
      -

      NeuroInformatics 2011

      -

      Informatics Forum, Edinburgh, UK

      - -
      -
    • - -
    • - -
      -

      NeuroML Development Workshop

      -

      Goodenough College, London, UK

      - -
      -
    • - - - -
    -
    -
    -
    - -
    -
    - - -
    - - - - -
    - - - - - - - - - - - - - - - + + + OpenWorm Events + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    +
    +

    OpenWorm Events

    +

    + Where we have been and where to meet us next +

    +

    + See events on Tumblr » +

    +
    +
    + +
    + +
    + +
    + +
    + + +
    + + + +
      +
    • + +

      Loading events from the blog...

      +
    • +
    + + + +
    +
    + +
    + + +
    + + +
    + + + + + + + +