Join us on IRC | How about logging in? | edev FAQ

edev is the 'Everything Developer' usergroup, for people interested in contributing to the development of this site, or just learning about how it works. Please /msg Oolong if you want to be added.

Contained in this document are instructions for how to get the ecore development environment to work. Note that I'm developing primarily on a Mac, and while the instructions are suitably generic and will probably work on Linux, adjustments might need to be made to get this to work on Windows.

Step 1. Download VirtualBox
VirtualBox is available from here. You'll need it, as it runs the underlying VM that we'll be working off of.

Step 2. Make sure that you have a ruby binary installed.

Step 3. Install Vagrant
Vagrant is our virtual machine infrastructure glue, and it's written in ruby. It will run the chef recipe needed to get everything working once you download everything. Grab it from the downloads page and install it. Your package management system might also have it, but note that as of 2013-04, the rubygems version is out of date.

Step 4. Sync the code from github
We're on github now, more as this continues. If you don't have a git binary in your path, you'll need one from ports or apt or wherever you get your software. http://book.git-scm.com/2_installing_git.html has instructions for how do do this. You'll need to set up your git environment some, like setting your username and email if you'll want to commit. http://help.github.com/set-your-user-name-email-and-github-token/ has how to do that. Lastly, with how to fork the repo to make commits, check out: http://help.github.com/fork-a-repo/. Anyone who had commit access to the repository before, I'd be happy to restore that access, or you can send me a git pull request.

Step 5. Boot up vagrant
I'll write up a vagrant primer in a bit, but here are the basics.
  • vagrant up - Starts your VM up, creating it from scratch if needed. Note that if you have never done this before, it has to download the base box from bonci.net, so please don't go getting rid of the box entirely. Note that it is still available if you do a vagrant destroy. You need to specifically trash the underlying box.
  • vagrant halt - Preserves the vagrant state and shuts the machine down gracefully
  • vagrant destroy - Trashes the virtual machine. Don't worry, vagrant up will recreate you a clean environment, but there's no way to get your data back.
  • vagrant ssh - ssh's into the machine as root.
All commands must be done from the everything2/vagrant folder. For instance to start it the first time:
cd everything2/vagrant
vagrant up


Note that it will take a little while to get it up and running the first time, as it is installing packages and configuring the system.

Lastly, in your VM host OS, go to http://localhost:8888. This will bring you to the frontpage of the ecore setup. All of the passwords have been set to 'blah' for now, so you can log in as root and away you go. Note that as of the time of these instructions, there are still a few rough edges which need to be smoothed, but we'll get there.

Final notes:
  • This is running the same version of perl and other software as production, so we should be seeing the same warnings
  • Stored procedures aren't captured in the node dump right, so they aren't imported properly. More on that later.
  • The root key to the ssh machine is in the vagrant subdirectory. It doesn't have a password
  • The root password, if you need it, is 'vagrant'

Purpose

An unobtrusive interface to e2.ajax.update() functionality. An element (identified by id attribute) is replaced or updated by the result of a call to an htmlcode. Text arguments can be passed to the htmlcode and query parameters included in the html query. The parameters can be specified, or they may be extracted from a form or an url specified in an href. They can also be requested from the user. Specification of what is to be updated (the target element) and how is provided through the class attribute of a trigger element. Trigger and target may be be the same, or one may contain the other. In one special case triggers are added automatically.

Implementation

The trigger element contains a string of the following pattern in its class attribute:

ajax<whitespace>[(]<target id>[)]:<htmlcode name>[?parameters][:arguments]

Parameters are specified in the same way as in an url (but see below for additional options).

Arguments are specified as a comma-separated list of strings.

The trigger element is activated:

  • If it is a form control and parameters are not specified, when the form is submitted.
  • If parameters are specified and it is not a select element, when the element is clicked.
  • If parameters are specified and it is a select element, when the selected item is changed.
  • If parameters are specified and it has the class 'instant,' when the page is loaded or the element is inserted into the page.

On activation of the trigger element, the htmlcode is called on the server with the arguments provided and the (content of) the target element is replaced with the result. If no parameters are specified, the parameters for the ajax call are extracted from the form to which the trigger element belongs if it is a form element or from its href if it is a link. If no node_id is specified in the parameters, the id of the current $NODE is included as node_id.

If the target id is in parentheses, only the content (innerHTML) of the target will be replaced.

Escaping spaces

To avoid spaces in htmlcode names or arguments leading to unwanted behaviour (extra classes) they must be escaped with '+'.

Additional parameter options

The user can be prompted to provide a parameter value by including the pattern:

<parameter name>=/#<prompt text without question mark>

On activation of the trigger, the user will be prompted to provide a value. If an empty value is returned, the ajax call will not happen.

If the trigger element is a form control, values can be extracted from it and other controls in the same form by specifying parameters using the pattern:

<parameter name>=/[<control name>]

If no control name is provided, the parameter name is used to identify the form control.

The same parameter may be specified more than once to provide defaults or allow for overriding of existing values from a form:

ajax x:y?op=z&msg=Fruitbat&msg=/msg1&msg=/#What+is+your+favorite+nocturnal+animal:123

will first set the parameter msg to 'Fruitbat', then replace that with the value of the control with name 'msg1' if it has a value, then prompt the user to confirm or change the animal or cancel the whole business.

Special case for Nodelets

Most forms in nodelets (those that don't need a complete page load to do their job) get an additional hidden input added automatically by Everything2 Ajax with name 'ajaxTrigger' and class 'ajax <nodeletid>:updatenodelet:<Nodelet+Name>'. If you don't want this, put a form control of your own with name 'ajaxTrigger' in the form.

Most links in nodelets pointing to the current node are automatically given class 'ajax <nodeletid>:updatenodelet:<Nodelet+Name>'. If you don't want this, give the link class 'ajax'.

E2 Mobile Interface

Having recently acquired an iPhone, I was in equal measures impressed with the mobile interfaces on sites like Facebook, Texts from Last Night etc, and horrified by E2's interface.

E2's already-cluttered user interface degrades severely on a mobile device with a tiny screen, as well as suffering from the low bandwidth inherent in these devices and the CPU time needed to render all that extraneous stuff.

For a long time I've been saying that E2's user interface needs to be decluttered and simplified, and a mobile device interface seems like the perfect excuse for doing just that.

Most of the clutter on an E2 page comes from nodelets that we rarely even look at on a pageload. They make for a lot of extra text and clutter, and placing them to the side of the page makes for a very wide page on a mobile device. But nodelets themselves are generally just about the right sort of size for a mobile display. So aiming at the maximum benefit in usability for the minimum amount of code changes, I decided to refactor the UI a little:

  1. Don't display nodelets on a page. Just the page please, ma'am.
  2. Display nodelets on a page of their own, one per page.
  3. A 'controls' page/tab gets the most commonly used bits of clutter. Currently, the search box and a list of the available nodelets. A meta nodelet meta-container, if you will.

While this vastly reduces clutter and improves usability on mobile devices, it retains more or less the full functionality of the site: the mobile interface has access to all the regular nodelets, and they behave pretty much exactly as they do on the regular interface. This isn't a reimplementation of the UX, it's just a refactoring.

Schema

general container (container)
Modified to detect 'mobile' mode by hostname or cookie, and set the parent container to a settings- or theme-driven mobile-specific parent.
zen mobile container (container)
Zen theme mobile container, sets the viewport and presents a main display without nodelets. Also displays control tabs (below). The 'wrapper' element is contained within a further 'mobilewrapper' element for easy identification in stylesheets.
zenMobileTabs (htmlcode)
Control tabs for the mobile interface.
Zen theme
Added 'generalMobileParent_container' key to point to zen mobile container.
node listnodelets page (htmlpage)
Display page for any node which presents the 'controls' view of that page. Mostly, links to nodelets, hence the displaytype name 'listnodelets'.
node shownodelet page (htmlpage)
Displays a single nodelet for a page, identified by the 'nodelet_id' parameter. Contains a fairly nasty hack to ensure that any links in the nodelet which preserve the displaytype of the current page will also preserve the 'nodelet_id' parameter.
zen user display page, zen writeup display page, zen e2node displayfull page
Annoyingly, these had their parent containers set to zen container rather than general container. It seems somewhat logical given that the only previous use of general container was to override parent containers on a per-theme basis. So I reset them to general container.
zen stdcontainer
To reduce the load imposed by google, the zen stdcontainer sets a 'base' for the page which effectively makes all links point to 'everything2.com' regardless of whether the link came from everything2.org or everything2.net. This scuppers attempts to use an HTTP hostname to determine whether to show the mobile interface or the regular one. So disable the base insertion for mobile hostname.
kernel blue
Stylesheets (more below) generally set the 'wrapper' element's padding to a fixed size to leave space for the nodelets along the side. On mobile devices, this amount of space is typically just about the whole screen. So I have modified kernel blue to override that, and also add some styling to the tabs presented by zenMobileTabs.

Stylesheet modifications

Additional elements are added (mostly by zenMobileTabs):

#zen_mobiletabs
div containing the tabs
.zen_mobiletab
Tab which is not selected
.zen_mobiletab_selected
Tab which is selected
#mobilewrapper
Wraps #wrapper in the mobile interface, so that the selector "#mobilewrapper > #wrapper" can be used to override #wrapper's styles (particularly, the padding for nodelets!)

Still to do

  1. Make other stylesheets play nice.
  2. We need a mobile-specific front page.
  3. Fix guest user caching so that guest user pageloads from the mobile hostname don't redirect to the regular site.
  4. Link from the regular site if we detect a mobile client device.
  5. Testing and feedback!
  6. Tabs and titles are in the wrong order: since the tabs select views within the page, the title of the page should be above the tabs. Probably a CSS issue more than anything else.
  7. Notifications: since the epicentre isn't on every page, we'll miss XP notifications etc. It would also be good to have a 'new messages' notification too.

Access

You can give it a whirl by going to: http://m.everything2.com/node/login.

As in previous months, I have concentrated on coding visible features to impress the masses, while the likes of Oolong and OldMiner grind away behind the scenes doing essential but less visible work to stop the site melting down or freezing up, depending on the weather and the current sign of the first derivative of the gravitational potential field with respect to distance from the centre of the Earth. And just to make sure everyone knows who to thank for the new bells, whistles, and errors here is a summary of what I have been doing. Much of it followed on 'logically' from the things I was doing before to improve writeup footers:

  • If you send a message to the author of a writeup from the blab box under the message, the report of the sent message is no longer jammed along with the blab box and their fourteen children in a microscopic flea-infested box where they have to stand on each others' heads in order to breathe, but has a well-ventilated space of its own under the writeup footer (or header, if you're really old-fashioned. Are you still wearing those socks?).
  • But why should a message have to wait to be sent to be able to breathe? Writeup message boxes now expand to accommodate the text within them, as do many others. There was already code in place to make multi-line textarea elements expand, but it couldn't be used for text input boxes, since these can only be used for a single line of text. So the input boxes have to be replaced by an expanding text area of the same starting size as the original text input, and then have to be watched to see if the user hits the return/enter key, whereupon the form containing the box is submitted, as is normal with an input box (but not with a text area).
  • Making the message box in the Chatterbox nodelet expand was less easy, because under some circumstances the entire chatterbox is replaced by the magic of AJAX and the expanding replacement message box would be replaced as well. AJAX is a technology for replacing bits and pieces of web pages instead of reloading the whole page, and is generally thought to be a Good Thing; you can enable it by selecting 'Everything2 AJAX' at the Javascript Repository. So the chatterbox AJAX code needed some rewiring: (1) to use an onsubmit function on the chatterbox form to trigger it, rather than event handlers on individual form controls and (2) to update the chatbox with only one AJAX call back to the server, rather than one call to sendMessage and one to updateNodelet. This in its turn required some reorganisation of the updateNodelet htmlcode to execute opcodes and to execute nodelet code in the same environment as when nodelets are inserted into a complete page, adjusting the values of the $NODE variable and the query parameters appropriately. A desirable side-effect of all of that was that the AJAXified Chatterbox is twice as fast and reports sent messages. Two desirable side-effects.
  • Meanwhile, OldMiner redefined the meaning of 'success' in the Everything2 AJAX update function, which did away with the distressing tendency of the Chatterbox nodelet to occasionally vanish completely. Building on that, a callback function in an E2AJAX call is now always called after any attempted update, not only after 'successful' ones, making it possible for the callback function to decide what do about failure. Such as provide a bland error message. Code is in place to provide informative messages and not just "(unknown error)," but I haven't seen it say anything useful yet.
  • Those of you who created a website back in the good old days will remember how you had to everything twice: once for proper browsers, and once for the one everyone was using. Those days are past. Nearly. But to make the AJAX chatbox work in Internet Explorer 8, a 'get' to had to be changed to a 'post' so that IE will not use a cached response. Telling it to tell the server not to use any response cached after 1970 is simply not enough. What a rebel that William is.
  • Since I was messing around with the Chatterbox, I had a look at chatterlight. This has completely differently structured html code from the rest of the site, for which reason I excoriate and anathemetise it, and spit on the graves of its Reverend Progenitors. I also made a candidate replacement for it, currently to be seen at Chatterlighter. Since this provides absolutely nothing of any use to users of this site that is not also provided by chatterlight, its candidacy is currently in abeyance. One day it may be customisable in Exciting Ways in the context of pioneering new options for customisation of the site as a whole, whereupon its bid will be renewed. Please do not hold your breath, as resuscitation is cumbersome and not always 100% successful.
  • To make Chatterlighter as a bog-standard Everything2 superdoc it was necessary to make it possible for pages to define their own nodelet set, overriding or adapting the user's normal nodelet options. Having done that it was possible to make sure that when a nodelet is viewed on its own page it will not also be shown in the page sidebar. Nodelets can contain forms that do things which are better not done twice, and they have id attributes in their html code which should only be used once in a given page, so that was a Good Thing.
  • When Chatterlight(er) is inactive for a long time it no longer puts up an alert and steals the focus from whatever interesting things were keeping you from following the current discussion of the relevance of boobies to post-modern deconstruction of late-feminist neo-Quaker Quantum Mechanics. Instead it simply notes that it has got bored, provides a button to wake it up, and goes to sleep.
  • For a long time there has been an option in nodelet settings not to show the chatbox topic. As a radical break with tradition, if you select it, the chatbox topic will not be shown.
  • The revised chatterbox code only needed a little revision to be used to deal with forms in nodelets in general. So now submision of nearly all forms in nodelets will lead only to the nodelet being reloaded, not the whole page. (The exceptions are the log-in form and forms whose whole purpose is to take you to another page.) For example, you can change the number of writeups shown in New Writeups without reloading the whole page.
  • If you are an admin you have another option in New Writeups. This is now managed in the same place as the choice of length.
  • Strange things happened to the section collapsers in some nodelets when they were updated with AJAX. This turned out to be because the Ajax Update node is a node of type fullpage, and fullpage nodes are sent through the parseLinks function, which turns things in [square brackets] into links. It is now possible for fullpage nodes to say they can deal with their own links, thank-you very much.
  • The chatterbox was AJAXed, but sending a writeup message still provoked a full pageload. Solution: a method of flagging that a form field should be used as the basis for an AJAX update, not a page load. This could then be recycled and extended to links and then to arbitrary elements to provide asyncronous handling of all sorts of actions: cooling of writeups, bookmarking, adding to categories, making users 'favourite!'... The code requires no more than a little gobbledigook in the class attribute of an HTML element to ajaxify it, so there will be a lot more of this going on going forward (as we say these days). Saves your time, the E2 servers' time, and U. of Mich.'s bandwidth. Activate AJAX now, before we do it for you!
More or less unrelated to the above stream of free association football boots the chemistry set and mathchmaker match made in hell for leather shoe soul kitchen table and chair, no bill:
  • A principle of Good Web Design is that no function of a site should rely entirely on client-side scripting (code that runs in your browser): for the case that your browser doesn't Do scripts, it should also provide the same functions 100% server-side. E2 does not always comply with this. One place it didn't was in C!ing writeups when 'cool safety' was engaged: a pop-up window appeared asking for confirmation. Unless you didn't have scripting available in your browser, in which case nothing happened... To deal with this in a generalisable way there is now code in place that will ask for confirmation of an action with a pop-up box if scripting is available, or ask for confirmation by clicking on a button provided for this purpose after a page-load if not.
  • Oolong had coded an option to hide low-reputation nodes automatically in the New Writeups nodelet. These nodelets are now also hidden in e2nodes unless you ask to see them. You can decide for yourself how 'low' is low.
  • To err is human, to get driven completely and unreasonably NUTS by the trivial failings of others even more so. To make it easier for you to avoid contact with the works of those you cannot tolerate at your current level of spiritual development, there is a link from Your Ignore List to the Pit of Abomination. But only if you are already ignoring someone.
  • The New Writeups Atom Feed is now entirely proof against being truncated: if it comes out too long, a shorter version is made.
  • All of this coding business would happen much more slowly if at all if we didn't have a development server to play with and break things on where they won't get in the way. It would be nice if more members of edev were doing so. To make things even more fun and risky it is now possible to edit a code patch and apply it in one operation. This would be a Bad Idea on the production server: if we have to mess around with code here rather than importing it from the development server after fixing it so nothing explodes, then we should be forced to think about it carefully. But it would be a Bad Thing if the code on the development server were different from the production server in the long term. To make it possible to keep the code the same on the two servers while taking account of the need for different behaviour there is now a flag in System Settings, accessible to code as $HTMLVARS{isDevServer}, which is set on one server and not on the other.

This log was written on a plane. The oxygen pressure on planes is only 60% of that at standard temperature and pressure, and you should therefore avoid doing anything requiring significant mental acuity whilst flying.

Happy New Year. There has been a lot going on. This is only what's my fault:

Visible changes

  1. New page header providing operations to perform on the whole page (bookmark, add to category, add to usergroup, editor cool).
  2. Additional functions added to writeup footers: add to category; for editors: hide/unhide writeup. Add to usergroup function improved. The full list of C!s is available whether a writeup is displayed on its own page or in an e2node.
  3. Said functions removed from the epicenter nodelet/zen epicenter.
  4. All writeups which can't be voted on are flagged 'unvotable'. The reason why they are unvotable appears in a tool tip on mouse hover.
  5. 'marked for destruction' notice removed (redundant: see previous).
  6. Many of these functions are contained in pop-up widgets and are hidden when not in use to save space. They open and close at a click. When scripting is not available, the widget-openers link to a version of the page with an opened widget.
  7. Pages and writeups that are in a user's E2 bookmarks have a note indicating this with a link to the bookmark list (currently the user's homenode, but this may change).
  8. Users can choose which usergroup pages they want to have the option of adding to at a page provided for this purpose, rather than being required to guess the function of 'hideify!' and 'showify!' links. There is a link to this page next to the menu it affects.
  9. Users can simply adjust what is shown in writeup headers and footers at Writeup Settings without completely removing any more refined settings they may have entered at Old Writeup Settings.
  10. Jukka users are missing some functions. They are encouraged to switch to the Zen theme using Jukka emulation and note anything that could be improved with Jukka Emulation to improve their E2 experience at Suggestions for E2. We have enough time to make that work; keeping Jukka itself working would take too much time.

Rationale and history: tidy up and improve the writeup footer.

Writeup footers give readers the opportunity to do something in response to a writeup: say they like it, hate it, or love it, talk to the author about it, bookmark it on a social network, or whatever. Editors can also delete it, protect it from deletion, or remove the protection.

Sometimes people may also want to bookmark a writeup, add it to a usergroup page, or add it to a category. Until this month they had to look elsewhere on the page to find the means to do these things. In the case of categories, they would not have found them. Editors wishing to hide a writeup from the New Writeups list had to turn to the list itself to find the links they needed. The plan was to put all of these functions together in the writeup footer.

The links to add bookmark writeups or add them to usergroups (those 'ify!' links) were in the epicenter nodelet. Obviously, if they were to be moved out of here for writeups, it would be inconsistent to leave them there for everything else. They moved to the top of the page underneath the page title, where they were joined by the other 'full page' functions from the epicenter (favorite noder, editor cool), which would have been lonely without them. Means were also provided to remove the actions for particular nodes or nodetypes. These 'page actions' were first made visible to editors for a transitional period, to allow them to use this function.

The writeup footer was already rather cluttered, particularly for editors. But editor functions are used relatively seldom, so it made sense to hide them when they were not needed. The link used to reveal them evolved into a flag showing the status of the writeup (Public, Hidden, X'ed, Restored, Insured) under the influence of articulate feedback from the users, and acquired those triangle thingies™ to show you where to click. Various improvements and theoretically redundant changes were made so that the widgets would be visible and on the right continent even in minority Scandinavian browsers and would not jump out of the way when tickled in smug shiny browsers.

Once the complaints had died down, enthusiastic suggestions were made to put many more functions, possibly all of them, into the widget. The argument prevailed that since the point of the widget was to save space, it made sense to have something to save space for. For those who wanted the old look to their killing tools, an opt-out was provided at Writeup Settings. Unfortunately, that page destroyed any old-fashioned header/footer options they might have had even if that was all that they changed, so that had to be fixed as well.

Since a writeup is a writeup wherever it is, there seemed to be no good reason to show its C!s differently depending on whether it was alone or in company. But a long C! list takes up space and can uglify your whole footer, so it was hidden in a widget.

The existing usergroup menu was enigmatic and dependent on javascript. It was replaced by a form that works with or without scripting and that tells you what you are doing. This reduces the likelihood of people adding things to usergroups by mistake, but if they do they are informed of what they have done and are given the option of undoing it.

The usergroup form was tidied away in a pop-up widget. Following a suggestion from Oolong, this widget also contains a bookmarking link and a form to add a writeup to a category. Categories are a much underused feature of E2, partly due to their almost complete invisibility. Now that they are more visible, it is to be hoped that the users will clamour for for more and better category functionality, and coders will enthusiastically respond. The results of this could include:

  • Tools to reorder the nodes in a category.
  • Options to show all or a part of the content of all of the nodes in a category on a single page.
  • Automatic provision of links at the bottom of a writeup to the series of writeups it belongs to (a category), and to the preceding and following writeups in the sequence.
  • Your idea goes here

At the top of the page, there is more room, so bookmarking, categories and usergroup page functions are shown separately.

Back-end changes to make it all work

(Most links here will only be of use to edev members and staff.)

  • basesheet provides basic styling for widgets.
  • default javascript gives the widgets their pop-up-ness.
  • voteit provides pop-up killing widget if its parameter & 4.
  • The new htmlcode widget will build you a widget if you ask nicely.
  • openform can take a parameter hash as arguments to put style and class attributes on forms.
  • Slight tweak to async voting to adapt to new voteit markup and make it possible to pass a writeup id as argument for a quick kill.
  • displaywriteupinfo changed to make voteit return a kill widget if not disabled by the user and to include the new Add to… widget.
  • Writeup Settings checks existing settings against a regex to see if it could have generated them. Provides explicit option to replace them if not. Only adds/removes individual options if this option is not checked.
  • page header provides a page header.
  • page actions provides a list of links and widgets to put in a page header.
  • Disable actions allows staff to remove action links from the page header.
  • disabled actions keeps track of which actions are disabled for what.
  • removeweblog no longer allows (and requires!) a user id as a parameter to remove a document from a usergroup page: access privileges depend on who the user is, not on which parameters they can hack. The user who linked a document can also remove it.
  • Epicenter contains information where the links used to be on where they can be found and how to put them there if they aren't there.

Other changes/fixes

The New Writeups Atom feed has been optimised to avoid it being truncated. The fix is not 100% bombproof: theoretically, under some very unlikely conditions, it could once again fail. A few more lines of code checking the length and trying again with shorter writeup exerpts if it is over 65000 are still needed.

Writeups are now displayed by a simple htmlcode that uses the show content htmlcode to do the heavy lifting.

CoolUnCoolIt shows C!s on writeups restored from Node Row.

Node Backup will not produce corrupted zip files if your writeup titles contain characters that are not acceptable in operating systems produced in Redmond, Cupertino, or All Over the Place.

Patch importer will still complain if you reload it with a patch id still in the url, but will then go on to show the patch list.

See the top part of the table here for what I've left out above.

Venerable members of this group:

jaybonci@, Oolong@+, dann, N-Wing, bol, Teiresias, edev Bot, Orange Julius, rdude, call, ascorbic@, Vice_hkpnx, gate, ReiToei, in10se, elem_125, Devon, dafydd, opteek, Two Sheds, Hexter, nosce, The Lush, timgoh0, craze, redbaker, Redalien, maxClimb, randrews, DTal, DonJaime, Gryffon, themanwho, resiak, visudo, raincomplex, albinowax, No Springs, loughes, WaldemarExkul, prole, moosemanmoo, Rapscallion, alex, DarkDigitalDream, jdporter, user-970414, rycerice, kthejoker, Scout, Aerobe$, OldMiner, quintopia, mcd, E2D2, Serjeant's Muse, BaronWR, Old_New, Auspice@
This group of 59 members is led by jaybonci@