Jump to content

Mark

Clients
  • Posts

    36,220
  • Joined

  • Last visited

  • Days Won

    114

 Content Type 

Downloads

Release Notes

IPS4 Guides

IPS4 Developer Documentation

Invision Community Blog

Development Blog

Deprecation Tracker

Providers Directory

Projects

Release Notes v5

Invision Community 5 Bug Tracker

Forums

Events

Store

Gallery

Everything posted by Mark

  1. Effective moderation features are essential for online communities. Forums, blog entries and member-to-member messaging are particularly attractive for spam bots and nuisance users alike. IPS Social Suite has always been best in class when it comes to moderation features with features like the free IPS Spam Service that are completely unmatched by other web applications. Over this series of 5 blog entries I'm going to introduce you to some of the new moderation features in the IPS Community Suite 4.0. Part 1: Setting up moderators Part 2: Approval Queue Part 3: Reports Part 4: Effective Moderation Part 5: Warnings Back in IP.Board 3.0, we introduced a feature which at the time we called the "Report Center". Before this, if a user clicked the "Report" button, it would send a personal message to all the moderators for the forum the post was in (seriously). The Report Center was one of my favourite features in IP.Board 3.0 - it provided a great way for moderators to collaborate on reports and know the action that was taken. For 4.0 - we wanted to make this even more useful. We had 2 goals:It should be easy to set up and use. It currently has lots of configuration options dotted around the Admin CP and can feel a little confusing ("statuses" have "points" which leads to different coloured "flags" on reports) - it should be much easier for moderators to see what's important. When viewing the report itself, moderators should be able to see all the information they need immediately, and take action, without leaving the screen, making it quick and painless to deal with reports. The first part was making it easy for users to submit reports (if submitting a report is difficult or time-consuming, users won't do it, which defeats the point of having the feature). We've made it so when clicking the "Report offensive content" button next to posts, comments, messages, etc. a modal window will pop up - here the user can optionally fill in a reason for their submitting the report, and when submitting, the modal window just disappears, with no page reload, so the user is not interrupted from what they're doing. Video Demonstration Next we wanted to improve how moderators deal with reports - here's a screenshot of the report screen: Right from this page I can:See the content that was reported - I don't have to click anywhere to view it (naturally, I can click on the title to be taken to the actual content if I want to see it in context). Edit or delete the content. If I do this, it will do it via AJAX, without me ever leaving the page. Video Demonstration See any warnings that have been given in the past to the user who posted the content being reported, and issue them with a warning (which will be issued without me leaving the page). Flag the member as a spammer, which will automatically take all the appropriate action for that, depending on how I've set it up in the Admin CP. See who has reported this content, and the message they provided with the report. By hovering on their photo I will see their hover card which allows me to send them a message (which will be sent without me leaving the page). See any comments on the report from other moderators and make a comment on the report - comments are submitted by AJAX so I can make a comment quickly. Change the status of or delete a report. Move to the next/previous reports pending.
  2. Effective moderation features are essential for online communities. Forums, blog entries and member-to-member messaging are particularly attractive for spam bots and nuisance users alike. IPS Social Suite has always been best in class when it comes to moderation features with features like the free IPS Spam Service that are completely unmatched by other web applications. Over this series of 5 blog entries I'm going to introduce you to some of the new moderation features in the IPS Community Suite 4.0. Part 1: Setting up moderators Part 2: Approval Queue Part 3: Reports Part 4: Effective Moderation Part 5: Warnings Sometimes content needs to be approved before it can be viewed. This can happen when:Approval is enabled for a particular member (perhaps for a particular time after giving a warning) Approval is enabled for a group (perhaps for new members until they have been registered for a certain number of days) Approval is enabled for a forum/category/etc. Currently, if there is content requiring approval, badges display next to the forum/topic to alert moderators. While this works well it has some drawbacks: it means clicking around the community to find content, and if there's an area of your community you don't visit very often (personally I don't often check the gallery here) sometimes you might not notice something needs to be approved. For 4.0, we wanted to improve this. There were two main goals we set: What we've created is a new area of the moderator control panel which we call the Approval Queue. When you visit the approval queue, you see the first topic/post/comment/whatever which is pending approval: As you can see, the page shows you clearly who posted it, what it is and the content. You can click on the badge on the right (in the screenshot above where it says "File Comment") to be taken directly to it if you want to see it in context. At the top, you can see 3 really clear actions: approve, skip and delete. Clicking any of these will do that action, and then immediately show you the next thing pending approval. This allows moderators to move through the queue really quickly and effortlessly. By clicking on the author's name, you can also issue a warning, flag the user as a spammer and send the user a message - all this is done without leaving the page: And when all content has been approved, you can enjoy the satisfaction of an empty queue: Here's a video of it in action: As an incidental feature - previously if a member made a post and it needed to be approved, they would get a confirmation message telling them so but wouldn't be able to see the post. This sometimes led to confusion when members missed the confirmation message and thought their post hadn't been submitted. In 4.0, users can now see their own posts which are pending approval: [*]Content from across the suite should be pulled into a single area for moderators so moderators can locate content pending approval manually. [*]Moderators should be able to act on content pending approval (usually by approving or deleting) quickly.
  3. Effective moderation features are essential for online communities. Forums, blog entries and member-to-member messaging are particularly attractive for spam bots and nuisance users alike. IPS Social Suite has always been best in class when it comes to moderation features with features like the free IPS Spam Service that are completely unmatched by other web applications. Over this series of 5 blog entries I'm going to introduce you to some of the new moderation features in the IPS Community Suite 4.0. Part 1: Setting up moderators Part 2: Approval Queue (New Feature) Part 3: Reports Part 4: Effective Moderation Part 5: Warnings Up until now, each application has been responsible for managing it's own moderator permissions (for example, you go and set up a moderator in the forums app, then in the gallery app, etc.) and there's been a concept of "global" or "super" moderators who can perform all moderator actions in all applications. In IPS Community Suite 4.0, we're centralising the creation and assigning of moderator permissions, and are doing this separate from groups (so you can now make just a member a moderator without putting them in a special "moderator" group). It works very similar to Admin Restrictions in 3.x. Here is the Moderators page: (In this screenshot I've given moderator controls to everyone in the groups Administrators or Moderators, and to the user "Brandon") When editing a moderator you see all of the permissions available across all applications. This screenshot shows global moderator permissions which apply across all applications: If you do not want to allow any of these globally, you can make them available only to certain areas - when any option is toggled off, the equivalent option will show under each application tab, along with an option to select which areas of that application it can be done in. For example, if I disable the "Can edit all content?" option, a "Downloads" tab appears with the following options: This allows me to choose what the moderator can edit, and in which categories they can do it. A similar tab appears for each application I have installed, or additional options appear on the tabs. Also when editing a moderator I can control permissions not related to content, for example, how they can use the warning system: Member management permissions: And more. When editing a moderator, I also have the option to "Give All Permissions" which makes them akin to "global" or "super" moderators in 3.x. When editing a member which has been given all permissions, I will see a message reminding me that if I remove any permissions they will no longer be a global moderator:
  4. Reminder: this blog covers the technical details of 4.0's programming. For details on 4.0's features, follow our main blog. Introduction For almost all applications in the IPS Social Suite (IP.Chat being the notable exception), there are three components: Each of these different types of items share many common features. For example, in all applications you can "follow" nodes and Content Items, you can like (or give reputation on) Content Items and comments. There's also searching, tagging, moderator controls (pinning, locking, etc.), sharing, reports and so on. Up until now, applications were largely in charge of managing these different components and their relationships themselves, and utilised often complicated extensions to implement the common features. In 4.0, these components are handled differently. Each component follows an , extending a central class for the component, and implementing interfaces to enable additional features. Working with objects So, taking IP.Board as an example, the classes for each of the components (forums, topics and posts) will start off like this: [*]Categories created by the administrator. For example, forums in IP.Board, categories in IP.Downloads, calendars in IP.Calendar). In 4.0, the terminology used throughout the code for these is "Nodes". [*]Content created by users, usually (though, not always) within categories. For example, topics in IP.Board, files in IP.Downloads, events in IP.Calendar, images in IP.Gallery, personal conversations). In 4.0, the terminology used throughout the code for these is "Content Items". [*]Comments posted on Content Items by other users. In most applications these are simply called "comments" though in IP.Board they are called "posts" and in IP.Nexus and IP.Downloads they take the form of reviews. Active Record design pattern namespace IPSforums; class Forum extends IPSNodeModel { ... } class Topic extends IPSContentItem { ... } class Post extends IPSContentComment { ... } In , I already talked about how Nodes work and showed how easy it is to start using them. Content Items and comments are the same, with very little additional programming (only specifying a few properties to specify what database table to use and the names of the other classes they relate to), we can start using them. For example, to get a topic from the database, I just do: an earlier blog entry $topic = IPSforumsTopic::load( 1 ); In this example, 1 is the ID number. If I was accepting user input, I could just wrap it in a try/catch statement. I could also, rather than using load() use an alternative factory method, loadAndCheckPerms(), which automatically checks if the currently logged in user has permission to view and throws an exception if not: try { $topic = IPSforumsTopic::loadAndCheckPerms( IPSRequest::i()->id ); } catch ( IPSContentNoPermissionException $e ) { IPSOutput::i()->error( "You do not have permission to view that topic.", 1, 403 ); } catch ( OutOfRangeException $e ) { IPSOutput::i()->error( "Could not find topic. It may have been deleted.", 2, 404 ); } In the object, properties match the columns from the database table. For example, to set the page title to the topic title, I just do: IPSOutput::i()->title = $topic->title; There's also lots of methods available. For example, to get the IPSforumsForum object of the forum the topic belongs to, I just do: $forum = $topic->container(); Or to get the IPSMember object of the member that posted the topic, I just do: $author = $topic->author(); An Example: Getting the latest 5 topics One thing which is particularly easier now is that now the central classes handle common functionality, you can easily obtain data without having to worry about if everything has been accommodated - the class handles it automatically. I already showed how loadAndCheckPerms() works - for a more complicated example, let's say you wanted to get the 5 most recent topics to display in the sidebar. Previously you'd have to do a query, joining on the permissions table, providing the permission mask IDs of the current user manually, remembering to check to exclude hidden topics (unless of course, the user had permission to view hidden topics). In 3.x, it would have looked something like this: $member = ipsRegistry::instance()->member()->fetchMemberData(); $topics = ipsRegistry::DB()->buildAndFetchAll( array( 'select' => '*', 'from' => array( 'topics' => 't' ), 'where' => ipsRegistry::getClass('class_public_permissions')->buildPermQuery( 'p', 'perm_2' ) . ( !$member['g_is_supmod'] ? ' AND queued=0' : '' ), 'add_join' => array( array( 'select' => 'p.*', 'from' => array( 'permission_index' => 'p' ), 'where' => 'p.perm_app="forums" AND p.perm_type="forum" AND p.perm_type_id=t.forum_id', 'type' => 'left', ) ) ) ); In 4.0, the same thing can be done with just one line of code: $topics = IPSforumsTopic::getItemsWithPermission( NULL, 'start_date DESC', 5 ); Naturally, one could have written a method to do this in 3.x, but in 4.0, because it is handled centrally, it is common to all applications. If a new feature is added which affects the functionality (such as when hiding content was added), each application does not have to be updated. Adding Features I already mentioned how there are certain features, like tagging, reputation, searching, etc. which are common to Nodes and Content Items throughout all applications. In 3.x, integrating these features involved writing a usually lengthy, extension. In 4.x, implementing most of these features is as simple as adding a few elements to your class. For example, let's take tagging. In 3.x, we have lengthy . It involves creating an extension, which in the IP.Board application totals 360 lines of code. In 4.x, you simply add an interface to your class - changing this: developer documentation for implementing tagging class Topic extends IPSContentItem { ... } Into this: class Topic extends IPSContentItem implements IPSContentTags { ... } That's all there is to it. Having done that, the form where a user adds or edits a topic will immediately gain a tags input field (the elements included on the form is handled centrally) and I can now call an additional method to get the tags on any topic so that I can display them in the HTML: $tags = $topic->tags(); Here is a screenshot of the full developer documentation for tagging in 4.x: The programming method employed here is actually more suited to traits, as implementing the interface does not involve adding any additional code to your class. The reason we've chosen to do it this way though is because traits are only a feature in PHP 5.4 and above, and we wanted to support PHP 5.3. It is likely that in a future version of IPS Social Suite we will switch to using traits. Other examples Reporting In 3.x: - 502 lines for IP.Board. In 4.0: Read Markers (shows if content has been read or not) In 3.x: - 146 lines for IP.Board, plus manually marking items as read. In 4.0: Liking / Reputation In 3.x: - 222 lines for IP.Board. In 4.0: Following In 3.x: - 357 lines for forums, plus 361 lines for topics in IP.Board. In 4.0: For content items: For comments: http://www.invisionpower.com/support/guides/_/advanced-and-developers/application/application-extension-reportplugins-r100 http://www.invisionpower.com/support/guides/_/advanced-and-developers/application/item-marking-r211 http://www.invisionpower.com/support/guides/_/advanced-and-developers/application/application-extension-reputationphp-r101 http://www.invisionpower.com/support/guides/_/advanced-and-developers/application/application-extension-like-r69
  5. Introduction The IPS Social Suite needs to store lots of different files - there's attachments and profile photos uploaded by members, CSS and JavaScript files, emoticons, etc. In IP.Board 3.x, various images got stored in different places: Files uploaded by users get put in the /uploads directory. If you have a complicated setup, it's difficult to handle these. If you have a load-balanced cluster you need to set up an environment whereby all files are stored on a single server, or all uploaded files are synched between servers, but serving these files over a high-performance CDN can be difficult. CSS, JavaScript files, images and emoticons get put in /style_* directories. If you want to serve these over a CDN, you can do so, but you need to copy the files over yourself. Other pieces of data are written to disk as a caching mechanism. This has the same issue with load-balanced environments as file uploads. Some applications had other methods - for example, IP.Downloads allows you to store files on a remote server using FTP. In 4.0, we wanted to pull this all together and build a much better system for storing files and build the whole system with high-performance environments in mind. File Storage In 4.0, you have several different ways to store files:On a local server On a remote server using FTP (which you can use to upload files to many CDN services) As binary data in the database On Amazon S3 You can set up different configurations and choose which configuration to use for different types of files. For example, if you want to store user's profile photos on Amazon S3, but you want attachments to be on the local server, or even a different Amazon S3 bucket - 4.0 can handle that. And if at any point you change your mind about which storage method you want to use, the system will automatically handle moving all the files for you. Everywhere that writes a file will use this central system - so IP.Downloads and IP.Gallery are included too. Caching There are lots of places throughout the suite where the same stuff needs to be retrieved or calculated over and over - for example, certain configuration settings, language data, information about the installed applications, etc. If this data can be cached, not only does it alleviate database load, it means the PHP code doesn't need to re-process the data. In IP.Board 3.x, some of this was stored in a particular database table and could be cached using a proper caching system - but it was difficult to configure, and not everything used it - compiled HTML templates, language strings and more were saved as files in the /cache directory, which causes difficulties for load-balanced cluster environments. In 4.0, we've overhauled all of this. For things that need cold storage (like compiled HTML templates) - you can choose either the file system or the database for storage. The data can then cached, along with anything else which might benefit from caching (like settings, application data, etc.) using one of 5 supported caching methods:APC eAccelerator Memcached Wincache XCache
  6. Introduction Modifications, add-ons, plugins, hooks - whatever your preferred name for them is - 3rd party code modifications are an important part of any successful web application. It wasn't that long ago that the way you did this was manually opening up files and copying and pasting bits of code in, or the really cool web applications had points scattered throughout the code for modifications to be injected into, or even scripts which opened up the files and made the changes for you (I'm not joking, that's seriously what used to go on!). In fact, IP.Board was one of the first web applications to, using OOP, support modifications in a more structured way. Currently, we largely have 2 types of modifications: applications, which add whole new areas and functionality to your site (all of our applications: IP.Blog, IP.Gallery, IP.Downloads, IP.Chat, IP.Content and IP.Nexus use this architecture) and hooks which modify or extend the functionality of the IPS Social Suite or of applications. Applications themselves are sort of self-governing so there isn't much to say about them, with one exception: applications will now be able to be downloaded and subsequently installed into your Admin CP as one file - you will not have to FTP upload application source files. The file will just be a regular .tar file, so course, if you were so inclined, you could open it and go old skool. For the rest of this blog entry, I'm going to focus on hooks. Though parts of this blog entry will be more technical in nature than our others, I've tried to keep it just to what everyone will be interested in, and leave the boring stuff until the end. Terminology The term "hook" in 3.x is ambiguous. Sometimes it refers to the whole thing (e.g. "install a hook") and sometimes it refers to a specific technical part of that - the code which overloads other code (e.g. "skin hook", "library hook"), which are, even more confusingly, sometimes called "hook files". In 4.0, we've decided to rename hooks to plugins. The technical parts which make up a plugin will still be referred to as hooks. Sandboxing Plugins, by their nature, extend functionality already present on your site. Up until now, if a plugin experiences a problem (for example, if a new version is installed which the plugin doesn't support) it can cause an error on your site, which disabling the plugin fixes. Starting in 4.0, plugins will be sandboxed. This means that if a plugin experiences an unexpected error (such as a database driver error), your site will automatically fallback to the default behaviour, and your users will never know anything went wrong. Simple (yet advanced) settings In IP.Board 3.x, the Admin CP maintained a massive central area for managing most (though not all) settings. Plugins could add settings to this area, though there was no real standard to where to do that. Also, because this area was separate from the area where you install plugins, it could sometimes be confusing how to configure a plugin after installing it. In 4.0, each plugin is allocated a settings page which is accessed just by hitting the "Edit" button on the list of plugins. Plugin authors can manage this page how they like - rather than being confined to the strictly tabular layout and specific input types in 3.x. Versioning In 3.x, unlike with applications, there was no particularly clear way to upgrade a plugin from one version to another. In 4.x, plugins now support full versioning, so you can just upload a new version, and an upgrader will take care of it. Hook Types In 3.x, there were several different underlying types of hooks: Action overloaders - which allowed overloading the PHP class for any controller. Library hooks - which allowed overloading the PHP class for some (though not all) other classes. Data hooks - which allowed the modification of variables at specific, defined places in the code. Skin overloaders - which allowed overloading the compiled PHP class representing a group of templates. Template hooks - which allowed content to be inserted at specific points in templates. For 4.0, we've made some quite radical changes: Code Hooks The first 3 have been merged into one concept we call "Code Hooks". Code Hooks can overload any class (even things which presently can't be overloaded like extensions) through a technique called monkey-patching (more details have been mentioned in the developer channel). This, combined with the use of Active Record models for all content items (so "Topic", etc. is a class that can be overloaded) also makes data hooks obsolete. Theme Hooks The last 2 have also been merged into a concept called "Theme Hooks" (we're also renaming "skin" to "theme"). The way the current template hooks work is to insert content around certain pre-defined tags in the template. The problem is, not always is the point the plugin author needs available, also this is done in a way the content being inserted isn't aware of it's surroundings, which makes it difficult for things like adding a button to every post, which would need to know information about that post. After thinking for ages about a better way to facilitate theme hooks (I was halfway through a system which injected hook points automatically at compile time), our designer Rikki reminded us that a pretty well-known method for selecting HTML elements already exists... CSS selectors. Video demonstration What's really cool about this is that the content used acts as if it was part of the template - if for example, it's inserted in a foreach loop, the variables created by that are available. It can also use template logic and everything else templates themselves can do. On the back-end, these are compiled into a file which behaves like a 3.x skin overloader - so if it is necessary (or just desired) to overload the compiled version of the template, that is still possible. Theme hooks work for the Admin CP as well as the front-end. Developer information Developers no doubt would like to know the technical information of how this all works. Rather than write a blog entry covering all the different parts of plugins, we thought you might be interested to just see the developer documentation. We have 2 articles we can show you - one covering all the technical details of plugins, and another which provides a step-by-step guide for how to create a plugin.
  7. One of the things we wanted to focus on for IPS Social Suite 4.0 right from the beginning was providing better support for sites which do not use English or use multiple languages (or, as it was scribbled on my whiteboard, "++ i18n/L19n"). In this blog entry I'm going to cover some of those changes and new features. Translatable Everything Currently when you create a forum, user group, custom profile field, etc. you have to give it a title and can only do this in one language. If you have more that one language installed, you might want to provide different titles for different languages. In 4.0 you can do exactly that - if you have only one language installed, these fields will continue to show as normal text boxes - however, if you have more than one installed you'll see several text boxes like this: Visual Language Editor One feature that has been really popular in IP.Board is the Visual Skin Editor - a tool which allows you to browse your site, and click on elements to bring up a colour selector to change it. What if we could take this idea and apply it to translating as well? Allowing you to click on any word or phrase on your site and translate it there immediately. In 4.0, you can. Easier Language Management In addition to the visual translation we've also made several improvements to the traditional translation method: As you search for a language string, results appear as you type. Editing a language string saves immediately without needing to click a save button. Filter tabs can show you words/phrases which have not yet been translated or the translation is out of date (meaning we've changed the default English value for the word/phrase since it was translated). We've also made importing/exporting much faster and more reliable - no matter how large your language is (it will grow as you add more applications of course) there is now no risk of hitting an error importing/exporting (for those interested in the technical side of how this is achieved, see this blog entry). An exported language pack will also now maintain information on the version of each application it was exported from, so that the filter which shows outdated language strings is always accurate. Automatic Language Detection Let's say you have Spanish and French languages installed on your site - up until now, you'd have to choose one default language, and users who want the other would have to manually choose it (which can be extremely difficult to find how to do when you're browsing a site in a foreign language). In 4.0, we automatically examine the information that the user's browser sends (which includes their preferred language) to choose the best one out of what's available, if that user hasn't already set an explicit preference. Pluralisation In English, pluralisation is very simple - for most nouns, you just append "s" on the end, with some variation for certain words. This however, isn't the case in all languages - for example, I was speaking with the owner of a site in Slovak recently who was telling me that the word "records" changes depending on the number of records there are - for 2 records, it's "2 články", but for 5 records it's "5 článkov". Currently, most language strings only have a singular and plural form (as is all that's needed in English) - meaning having the site show "2 články"/"5 článkov" was impossible. In 4.0, we've introduced some really basic logic into language strings to accommodate this. Rather than having, for example, two language strings with the singular and the plural, there is now one with a value like this: {# [1:record][?:records]} The # indicates where the number will go, then each set of square brackets represents a possible value - the number before the : indicating the number which will cause that to show, and ? meaning "all other numbers". So for our Slovak example, we'd set the value to: {# [1:článok][5:článkov][?:články]} On display, it will automatically show the appropriate version. Lists Along a similar thread to pluralisation, we've also made the way lists are formatted to be customised through a special language string. For example, a list in English looks like "one, two and three". However, in Japanese, it's "一、二、三。" (the comma symbol is different and there's no "and") - similarly Arabic, Thai and others have similar differences. In 4.0, simply by changing an example language string, this can be changed. In the default language, this language string is: a, b and c For our Japanese example, we'd just change it to: a、b、c UTF-8 Without wanting to get into too much technical detail - UTF-8 is the most common of many ways text can be encoded for storage and display on webpages. UTF-8 has been the default encoding in our software since IP.Board 3.0. Some sites which have been around for a long while though may not be using UTF-8. This can cause issues with some features where UTF-8 encoding is expected (for example, many features which rely on JavaScript require UTF-8 due to JSON only supporting it and nothing else). In addition, some sites may try to use UTF-8, but content is actually stored differently as the database is set to a different encoding, which can also cause issues. In 4.0, we're going all UTF-8. If you're not already on it, the upgrader will convert data. This means a much more reliable and compatible way of handling text.
  8. The IPS Social Suite stores skin and language (and some other stuff) in xml files which are imported into the database at installation and upgrade. The reason we do it this way is so of course, you can export skins and languages and install them on other sites or distribute them via the IPS Marketplace. I'm not the biggest fan of PHP's XML handling at the best of times (it would seem whoever wrote the SimpleXML class and I would disagree on the definition of "simple") and had already changed some of the places we use XML to use JSON instead, but these skins and languages are particularly difficult because they can get huge. Handling these large files, especially when combined with sub-par servers can lead to memory exhaustion or maximum execution time timeouts. Our current method was taking both too much memory, and too much time in a single HTTP request. So we had to come up with something better. At first we considered splitting the XML file into several, but that would mean either requiring people installing a new skin/language to upload multiple files (which we deemed not acceptable) or compressing them in some way (which is another of PHP's weaknesses, and would have just created another problem). We also researched different markup languages, however found similar problems with everything. We also had to make sure that any solution didn't step on the toes of any of our other 4.0 goals, in particular we want to completely eliminate writing cache files to disk so as to better support cluster environments. The solution was two-fold. To resolve the memory problem, we decided to use the XMLReader and XMLWriter classes - these are non-cached, forward-only classes for reading and writing XML. Rather than store the document in memory, you can only read/write one node at a time, and can only move forward, not backwards. Even with this approach though, we needed to account for the fact that importing is an intensive operation anyway, and one that needs to be staggered. To accommodate this, we wrote a simple AJAX-based "redirector", which continuously fires HTTP requests at a PHP controller until the controller reports the import is finished (it displays a fancy progress bar to the user while this is happening). We decided to make the AJAX redirector a helper class so that it can be used elsewhere and by third-party developers (rather than our current approach in some areas of making the user sit through loads of physical redirects). The code is really simple: IPSOutput::i()->output = new IPSHelpersMultipleRedirect( /* Query string which will take us back to this controller */ 'app=foo&module=bar&controller=baz&do=process', /* Code to run for each HTTP request */ function( $data ) { // If we're not done yet... return array( $data, // Data to get passed back to this function, "Processing...", // Message to show (via AJAX) to the user 50, // [Optional] Number between 1 and 100 to indicate progess for progress bar ); // If we're done return NULL; }, /* Code to run when we're done */ function() { // Code that runs when we're done } );
  9. Charts and graphs are an essential tool in modern web applications. The API we use for displaying charts and graphs in IP.Board presently is something we wrote in-house during IP.Board 2.x - it uses the PHP GD library to generate an image representing a chart. At the time, it was pretty amazing, but as times have changed a number of libraries for generating much more visually appealing and interactive graphs have emerged. For IPS Social Suite 4.0 we've decided to retire our GD-based charting library and embrace something a bit more modern and familiar for developers to work with. After looking at a number of different solutions, we decided to use Google Charts (although wrapped with a simple gateway PHP class). Google Charts look great, have great features, are commonly used (so we figure any third-party developers who want to add charts to their apps will be able to do so easily) and is a service provided for free with no API limitations. I'll show you an example of how simple it is to create a chart in IPS Social Suite 4.0. Let's look at the code I might use to show a graph of the number of members registered over time: /* Init Chart */ $chart = new IPSHelpersChart; /* Specify headers */ $chart->addHeader( "Date", 'date' ); $chart->addHeader( "Members", 'number' ); /* Add Rows */ $stmt = IPSDb::i()->build( array( 'select' => "COUNT(*) AS count, DATE_FORMAT( FROM_UNIXTIME( joined ), '%Y-%m-%d' ) as joined_date", 'from' => 'core_members', 'group' => 'joined_date', 'order' => 'joined DESC', ) ); $stmt->execute(); while ( $row = $stmt->fetch() ) { $chart->addRow( array( new IPSDateTime( $row['joined_date'] ), $row['count'] ) ); } /* Output */ IPSOutput::i()->output = $chart->render( 'LineChart', array( 'title' => "Registrations", ) ); As you can see, the code is extremely simple to understand and use. This is what the output looks like: By hovering over any point I'll see a tooltip with the value at that point. All of the chart types and options available in Google Charts are available to us. Let's make some changes to make the lines curved, get rid of the legend and show titles on each axis - I just change the last line of the code to: IPSOutput::i()->output = $chart->render( 'LineChart', array( 'curveType' => 'function', 'hAxis' => array( 'title' => 'Date', ), 'legend' => array( 'position' => 'none', ), 'title' => "Registrations", 'vAxis' => array( 'title' => 'Number of registrations', ) ) ); And now my chart looks like this:
  10. To round up our previous blog entries on the post editor in IPS Social Suite 4.0, there's just a few extra features not previously mentioned to show off. @mentions @mentions are a common feature on social media sites like Twitter and Facebook. If you type an @ symbol and then start typing the name of a friend, an autocomplete menu shows so you can quickly then click on the user and they'll receive a notification that they've been mentioned. In 4.0 you can do exactly this to mention any user. Automatic Saving Currently, when you're typing a post, every 2 minutes the content of the post is saved, so that if you accidentally navigate away from the page, your post content can be recovered. The content is saved by making an AJAX request. In 4.0, we've rewritten this to use HTML5 web storage. This unloads this work to the browser, meaning no call needs to be made to the server. Because this is much more efficient, the save can be done much more frequently (every few seconds). This makes the autosave feature much more useful. In addition, we've expanded the feature to support attachments. So if you've uploaded files, these too will be automatically recovered. Essentially if you're in the middle of typing a post and you refresh the page, everything will reappear exactly as you left it. HTML Posting If you allow some users (like administrators) to post arbitrary HTML, they will see an additional "Source" button on the editor. When clicked, this will show them the raw HTML for the post and they can manipulate it here
  11. Introduction Joining my previous entries about content and uploading features in post editor in IPS Social Suite 4.0, I'd like to take you through the customisation features on the editor. Toolbar layout The buttons that appear on the toolbar are completely customisable in 4.0 and you can set different layouts for desktop, tablet and mobile (so that you don't show more buttons than the device can show). This is what the management screen looks like: (This is an unfinished design - the tabs won't be be like that in the final version.) To move a button you just drag and drop. The buttons on the right allow you to add more rows or separators. Clicking on a button brings up a dialog where you can adjust where and to whom it shows: Adding Buttons There are two ways to add a button to the editor. The easiest way is to install a CKEditor plugin. CKEditor has loads of plugins, and installing is as easy as uploading the zip file from their site. Here's a screenshot of the symbol plugin being used: The second way is similar to how custom BBCode currently works, you specify the HTML code to be added when the user clicks on the button. Manually created buttons can optionally have a dialog popup to ask for an option. Design Just as you can install CKEditor plugins by uploading the zip file, you can do exactly the same with CKEditor skins to change the design of the editor. You then simply set for each skin on your community which CKEditor skin to use for it. BBCode Though no features in IPS4 insert BBCode-style tags into the editor (like is currently done for attachments, etc.) users can still type BBCode into the editor and it will work fine. We've rewritten how BBCode is parsed to be much more secure and reliable and produce more standards-compliant HTML (for those who are interested, it parses the post content into a DOM Document and examines only the text nodes for BBCode tags, then either splits the nodes surrounding it and inserts one for block-level elements, or wraps all subsequent text nodes in the appropriate formatting element until the end BBCode is found). The benefit to this is that there now no longer needs to be a "BBCode mode" - you can type BBCode straight into the editor, even complicated stuff like lists spanning multiple lines, and it comes out looking great. The downside to this approach is that custom BBCodes can no longer be added through the Admin CP. However, as mentioned above, we now have the ability to add custom buttons to the editor which work in a much more intuitive way, and can do everything that custom BBCodes could and more. For those who really want to be able to add the ability for custom BBCode, we've isolated the method that returns the supported BBCode (and information needed to parse them) into a specific method so that custom BBCode can be added with a very simple hook specific to that purpose. Conclusion There's still one more blog entry to go in our series on the editor. To finish up I'll be showing off some cool special features including how you can post using regular HTML.
  12. Introduction In the last blog entry I introduced some of the features in the post editor in IPS Social Suite 4.0. In this blog entry I'd like to show you the uploading features in the editor. Using the "Image" and "Attachment" dialogs Along the bottom of the editor there are two buttons that deal with uploading files: image and attachments. Both present a dialog which looks like this: We decided to keep both an images and an attachments dialog as users wanting to insert an image will naturally look for the "Image" button - if however, you upload an image to the attachments dialog, it will work completely as expected. The upload panel here is based on HTML5 which supports drag and drop uploading, if your browser doesn't support this, it will use Flash, Silverlight or Google Gears if you have any of those installed, and if not it will fallback to a HTML4 & JavaScript implementation (none of these support drag and drop, but instead you click the "Choose Files" button just as you do now - the label in the box will change to reflect this). Uploaded files then show below the box (images will get a preview), and you can click on any to add them into the editor, or click the "Insert All" button. When you insert an attachment into the editor, it displays either the image if it's an image, or a link if it's anything else, just as it will actually appear in the post (rather than the current "[attach=XXX]" tag). You can also of course delete the attachment, which will automatically remove it from the editor if you've already inserted it. Video Demonstration Quick drag-and-drop In addition to interacting with the panels, if you're using a supported browser, you can drag and drop straight into the editor. It will automatically figure out whether the uploaded file(s) are images or other files and add them to the appropriate panel automatically. Video Demonstration Image URLs In the image panel, there is an additional "From URL" tab which allows you to insert an image from a URL, as you type the URL a preview is shown, and you can optionally link to the image. Video Demonstration My Files In IP.Board currently, there is a "My Media" button which allows you to insert content submitted either in other posts or elsewhere in the community (images in IP.Gallery or files in IP.Downloads for example) into the editor. In 4.0, this feature is found in the images and attachments dialogs. Just with normal attachments, the content is inserted as it will be shown rather than the current "[sharedmedia=XXX]" tag. Conclusion Please let us know what you think of the uploading features in the comments. Remember though that we're only half way through our series on the 4.0 editor. In my next blog entry I'll be talking about customising the editor and the place of BBCode.
  13. Introduction The post editor is undoubtably one of the most frequently used features of the IPS Social Suite as it's the way users submit content to your community and functionality has evolved dramatically from the early days of forum software which consisted of a plain textbox in which users would type BBCode into the feature-rich WYSIWG (What You See Is What You Get) editors prevalent on the web today. For the IPS Social Suite 4.0 we really wanted to focus on making the editor as good as it can be: feature-rich, intuitive to users and highly customisable. Over the next 4 blog entries I'm going to cover the functionality of the editor and related features (attachments, emoticons, etc.) and the customisation options that will be available in 4.0. The theme of each blog entry will be: Here's a screenshot of what it looks like (by default, on a desktop, more on that later): Quotes A feature added to IP.Board in the 3.x series was "visual quotes" - when adding a quote the box shows directly in the editor as it will show in the post, rather than as just normal text with a quote BBCode wrapped around. In 4.0 we've rewritten how this works to use a CSS3 based solution rather than JavaScript to keep the citation header attached to the quote, which makes the feature much more reliable and easy to use when splitting quote boxes, moving the cursor before or after the quote box or dealing with embedded quotes. Code We've also rewritten how code can be inserted into the editor to be more reliable. Adding code now initially brings up an editor which supports syntax highlighting: And after inserting, you'll see the code exactly how it will be displayed after posting and you can interact with it exactly how you'd expect: Spoilers Spoilers are now also visual (though of course they're not true WYSIWYG as you need to be able to see what you're typing) and a built-in feature enabled by default. When you click the spoiler button you'll be given a darkened box in which to type the content: The spoilers display in the post as the same coloured box with a "click to view" message. When clicked, the dark box fades away revealing the content underneath. Emoticons We've completely overhauled emoticon management in 4.0. You can now create multiple "groups", drag and drop to reorder and quickly upload loads of emoticons at once. When you click the emoticon button in the editor, you'll see a popup with an overview of all the groups. If your community only uses the default emoticons, this is big enough to show all the emoticons, however, if you like to install lots of emoticons, you can select any group from the dropdown menu to show all emoticons in that group, or use the search bar to find a particular emoticon. In addition, your most recently used emoticons will show right at the top. Embedded Media In 3.0 we introduced a media BBCode tag. By wrapping a URL to media such as YouTube or Vimeo videos in media BBCode tags, the correct embed code would automatically be worked out and embedded into the post. In later versions, we added automatic embedding support, so just pasting the URL into the post would cause the video to be embedded. This still works exactly the same in 4.0 with a couple improvements: Previews If you type the URL to a supported media in the "Link" dialog, a preview will be shown in the dialog - you can choose to insert the embedded media or just a regular link. If you insert the embedded media, it will show directly in the editor. oEmbed Currently URLs which gets picked up for media embedding and the output they produce are managed in the Admin CP. The idea was that this would allow admins to add support for additional services, however, it has caused some problems when for example, YouTube has added new URL formats or changed their embed code and they don't work on most communities until we release an update to change the default record for YouTube. is an API which allows you to make a call to the provider giving them the URL and they respond with the best embed code to use, if that URL can be embedded. In 4.0, if a URL for YouTube, Flickr, Vimeo, College Humor or Hulu is inserted, we make a call to their oEmbed provider to obtain the embed code, so it's always up to date. Conclusion This is just the first in our series of blog entries on the editor in IPS4. Please do let us know what you think in the comments, but remember there's much more to come! [*]Content [*]Uploads [*]Customisation and BBCode [*]Special features Video Demonstration Video Demonstration Video Demonstration oEmbed
  14. Mark

    4.0 - Trees

    Introduction I previously wrote a blog entry about building tables in IPS Social Suite 4.0. Similar to tables, we also have trees. Trees in many ways look and behave similarly to tables, but can be distinguished mainly by the fact that trees show a collection of objects in a fixed order (often, though not always the order can be changed by the administrator) whereas tables can show data sorted however you like at the time. An example of trees would be the list of forums in IP.Board. Trees vary quite significantly in their individual implementations, for example: Objects in a tree often have parent/child relationships. Sometimes this relationship is between the same type of object (for example, forums in IP.Board or categories in IP.Downloads), sometimes the relationship is between different types of object (for example, applications and modules, where modules are always the child of applications) and sometimes it's a mix of both (for example, packages in IP.Nexus are always children of package groups, but package groups may or may not be children of each other). Objects in a tree can usually, but not always be reordered by the administrator. Objects in a tree can sometimes be enabled and disabled without being deleted, for example applications and modules. Sometimes individual objects can't when the rest can (for example, you can't disable the "Core" application). Trees usually have controls for adding, editing, assigning permissions, duplicating and deleting objects, though a subset of these controls, or additional controls may be available (for example, you can't edit the permissions on a package in IP.Nexus, but you can run a purchase report, which you can't do in any other trees). For IPS Social Suite 4.0, I wanted to create a central class to create trees - currently we duplicate a lot of functionality, and all our trees display differently (in some places, quite radically so), but without loosing any of the flexibility necessary given the various differences in each implementation. Because trees are both more complicated and flexible than tables, the method for creating one might seem complicated - however, when you compare it to having to write every part manually (including all the JavaScript) as was the case in IP.Board 3, you'll be shaving hours off of development time. There are two ways you can create trees in IPS Social Suite 4.0. The most common way is when each item in the table has a record in the database. There are occasions when this isn't the case (for example, the developer center displays trees created from JSON objects), and the Tree class can handle these, but in this blog entry I'm going to show you the more common method. Just like I did for tables, I'm going to take you through how I programmed a real-world example, specifically the tree for custom profile fields. Creating the classes Custom profile fields are arranged into groups, so I'm going to start by creating a tree that shows the groups. To do this, I need to create a class for the custom profile field groups, I'll set this class to extend IPSNodeModel which is an abstract class that provides most of the functionality we need. IPSNodeModel in turn extends a class called IPSPatternsActiveRecord which provides functionality to make a class an Active Record (by that I mean, an object of the class corresponds to a record in the database). In this class I need to define a few variables - I'll explain them below, but this is the code I'll write: /** * Custom Profile Field Group Node */ class _Group extends IPSNodeModel { /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] Default Values */ protected static $defaultValues = NULL; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'core_pfields_groups'; /** * @brief [ActiveRecord] Database Prefix */ public static $databasePrefix = 'pf_group_'; /** * @brief [ActiveRecord] ID Database Column */ public static $databaseColumnId = 'id'; /** * @brief [Node] Node Title */ public static $nodeTitle = 'profile_fields'; } $multitons and $defaultValues are required by the IPSPatternsActiveRecord class. We don't need to do anything with them other than declare them. $databaseTable tells the IPSPatternsActiveRecord class what database table the records we need are in. $databasePrefix tells the IPSPatternsActiveRecord class the prefix used on all database columns. This isn't necessary, but since all the columns in my table start with "pf_group_", putting it here will save me typing it out every time. $databaseColumnId tells the IPSPatternsActiveRecord class which column contains the primary key. $nodeTitle tells the IPSNodeModel class what language string to use as the title on my page that shows the tree. Now I need to create a controller to display my tree - the developer center will set up the structure of this for me, then I just fill in the name of the class I just created: namespace IPScoremodulesadminmembersettings; /** * Profile Fields and Settings */ class _profiles extends IPSNodeController { /** * Node Class */ protected $nodeClass = 'IPScoreProfileFieldsGroup'; } Now I have a page which looks like this: Customising the rows You'll notice I now have two rows (because I have two rows in my database), but both are blank. This is because the IPSNodeModel class doesn't know what to use for the record title. Let's fix that by adding a method to our class: /** * [Node] Get Node Title * * @return string */ protected function get__title() { $key = "core_pfieldgroups_{$this->id}"; return IPSLang::i()->$key; } That code might seem a bit confusing, but note: $this->id gets the value of the "pf_group_id" column in the database. This is because the IPSPatternsActiveRecord class provides us with a __get method for retrieving the database row values. This is handy if we want to be able to modify the value returned for whatever reason, as we can override that method. We're retrieving the value for a language key rather than some kind of title field in the database. This is because in IPS Social Suite 4, data like this will be translatable, so if you have more than one language you can display different values depending on the user's language choice. So now we have this (I've clicked the dropdown arrow so you can see it's contents): Most of this is okay, but permissions aren't relevant for custom profile field groups, so let's get rid of that. The IPSNodeModel class has methods for checking if the user has permission to each button it displays (which by default check ACP restrictions, which I'll explain more about later) - we can simply override the method which checks for that button so it always returns false: /** * [Node] Does the currently logged in user have permission to edit permissions for this node? * * @return bool */ public function canManagePermissions() { return false; } And now it's gone: Making the buttons work The first buttons I need to make work are the add on the "root" row, and the edit on each row below that. I'm going to ignore the add button on each record row for now as that is for adding a child record and we haven't got to that yet - it will start working automatically when we add support for children. These buttons will display a form. In a previous blog entry I talked about our form helper class. I'm going to use this to build the add/edit form. To do this, I'll add two methods to my class to display the form and to save it's values - here they are: /** * [Node] Add/Edit Form * * @param IPSHelpersForm $form The form * @return void */ public function form( &$form ) { $form->add( new IPSHelpersFormTranslatable( 'pfield_group_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_pfieldgroups_{$this->id}" : NULL ) ) ) ); } /** * [Node] Save Add/Edit Form * * @param array $values Values from the form * @return void */ public function saveForm( $values ) { if ( !$this->id ) { $this->save(); } IPSLang::saveCustom( 'core', "core_pfieldgroups_{$this->id}", $values['pfield_group_title'] ); } Most of that should be self explanatory - however in the blog entry about forms I didn't mention the Translatable class. If you have one language installed, this will just display a normal text field, however, if you have more than one, it will show one for each language. It then returns an array, which we can pass to IPSLang::saveCustom() to save the values. This is what our form might look like if I have several languages installed: Or, more commonly, if I just have one: Now the add and edit forms are working, but since these are very small forms (they only have one input field) I'd like them to display in a modal popup rather than take the user to a new page (if the user has JavaScript disabled, a new page will do). To do this, I just add a property to my class: /** * @brief [Node] Show forms modally? */ public static $modalForms = TRUE; Next we have the copy and delete buttons. These will actually work by themselves (the central class will handle copying and deleting the records from the database), however, since we have translatable values, we need to make sure those too are copied and deleted appropriately. To do this, I'll override the two methods which handle copying and deleting: /** * [ActiveRecord] Duplicate * * @return void */ public function __clone() { $oldId = $this->id; parent::__clone(); IPSLang::saveCustom( 'core', "core_pfieldgroups_{$this->id}", IPSDb::i()->buildAndFetchAll( array( 'select' => '*', 'from' => 'core_sys_lang_words', 'where' => array( 'word_key=?', "core_pfieldgroups_{$oldId}" ) ), 'lang_id', 'word_custom' ) ); } /** * [ActiveRecord] Delete Record * * @return void */ public function delete() { parent::delete(); IPSLang::deleteCustom( 'core', 'core_pfieldgroups_' . $this->id ); } Search You'll notice that the system has automatically added a search box at the top of the table. In order to make this work, we need to add a simple search method: /** * Search * * @param string $column Column to search * @param string $query Search query * @param string|null $order Column to order by * @return array */ public static function search( $column, $query, $order ) { if ( $column === '_title' ) { $return = array(); foreach ( IPSLang::i()->searchCustom( 'core_pfieldgroups_', $query ) as $key => $value ) { try { $return[ $key ] = self::load( $key ); } catch ( Exception $e ) { } } return $return; } return parent::search( $column, $query, $order ); } Making the rows re-orderable The last thing I need to do to finish the handling of groups is make it so we can drag and drop to reorder them. To do this I just add another property to my class telling IPSPatternsActiveRecord which column contains the order ID: /** * @brief [Node] Order Database Column */ public static $databaseColumnOrder = 'order'; ACP Restrictions It's important of course to make sure we honour ACP restrictions. The easiest way to do this is to create individual ACP restrictions for add/edit/delete (this can be done in the developer center) with a common prefix, and then specify like so: /** * @brief [Node] ACP Restrictions */ protected static $restrictions = array( 'app' => 'core', 'module' => 'membersettings', 'prefix' => 'profilefieldgroups_', ); The system will now look for ACP restrictions with the keys "profilefieldgroups_add", "profilefieldgroups_edit" and "profilefieldgroups_delete" when performing those actions. Children Let's recap what we have so far with a video: http://screencast.com/t/TUBuuYBSBON Now that we have groups sorted, we're going to create another class for the actual fields which will show as children. The process is almost exactly the same as for groups. Since the process is the same, I won't go through the process step-by-step, but here is the class I've written if you're interested: Field.php The only changes are: I've declared two additional properties specifying the class name of the parent ("IPScoreProfileFieldsGroup") and which column contains the parent ID. I've declared an additional method to fetch an icon for the row so we can see what type of field this is. Just like we overwrote canManagePermissions for groups, I've also overridden canAdd in the same way, as you cannot add children to profile fields. Now all we need to do is link them up. To do this, I add a property to my group class telling IPSNodeModel the name of the class which contains children: /** * @brief [Node] Subnode class */ public static $subnodeClass = 'IPScoreProfileFieldsField'; The system will now automatically change the behaviour of our page in the following ways: When clicking on a group, it will expand out to show the fields under it. The search box will include fields as well as groups in its results. When clicking the "Add" button for a group, it will show the field to add a field to that group. When clicking the "Copy" button for a group, you'll have the option to copy children too or not. When clicking the "Delete" button for a group, you'll have the option to move children elsewhere or delete them too. (This is my favourite feature) In addition to being able to drag and drop fields to reorder, you can drag a field out of one group and into another. Here's a video: http://screencast.com/t/5fQwgle3EX
  15. A few weeks ago, I posted a blog entry mentioning a new feature in 4.0 which aims to make development of applications within the IPS Social Suite (both for us and third party modification authors) easier. We focussed on managing the database schema in that blog entry and I'd now like to take you through the other features. Modules Two tabs (one for admin modules and one for front modules) allow you to view all modules and sections in your application. You can add modules (which will both insert it into the database and create the relevant files in the filesystem), change the default section for a module (which previously required a defaultSection.php file) and create new sections. When you're creating a new section, the form looks like this (this is for creating a section for an admin module): The "Type" field controls the code that will be placed in the file created for the section - the options are: "Blank" - which will create the class with no other logic, so the section will be blank. "Table" which will create a class with a boilerplate for displaying a table. "Node Controller" - which will create a boilerplate for displaying a tree of containers such as IP.Board forums, IP.Downloads categories, IP.Nexus groups, etc. We've not posted how this class works, but a future blog entry will give more details. No matter which type you select, the system will automatically generate a file, with a basic class structure already filled in, including ACP restrictions checks, etc. The "Menu Tab" field is admin specific and controls under which tab in the Admin CP the section should show (for example, we have some stuff from the "core" app under the "Look & Feel" tab). Previously, one would have to edit the menu.xml file for the module to add sections, and making sections appear under tabs other the default application tab was very difficult - the new system does it for you. The "ACP Restriction" field is also admin specific and allows you to select an existing ACP restriction which will control who can see the section. It also has a special "Create Restriction" option, which will cause the system to create a restriction, and use that. Previously, one would have to edit the permissions.xml file to create restrictions and then assign them in the menu.xml file. Of course - you can completely bypass this feature and manually create your module folders and section files, but the addition of this feature makes the process much quicker. Admin CP Menu This tab contains a graphical representation of the data which was previously stored in menu.xml files. Admin CP Restrictions This tab contains a graphical representation of the data which was previously stored in permissions.xml files. Extensions Extensions are ways in which applications interact with one another. Previously, you would drop extension files in your applications "/extensions" folder, though there wasn't much reasoning to the structure of the directory, it was difficult to know what extensions were available, and sometimes understanding an extensions requirements was difficult. In 4.0, the extensions directory is more structured - the format is owner app > extension type > extension file (so admin/group_form.php for example, is now core/GroupForm/*.php) so this tab provides a GUI for managing your applications extensions. Applications can also specify a boilerplate file for an extension, so you can see what extensions are available, and clicking the "add" button will create a file with a basic structure to get you going. Settings In 4.0, developers have much more control over how settings are presented, rather than all being dumped in the central "System Settings" table. With this, much of the data that was previously needed in settings.xml is no longer required, so we've simplified the process of creating setting to just providing a key and a default value. Versions The versions tab shows all of the application's versions and the database queries that the upgrader will run when upgrading to that version. It's sort of a combination of the versions.xml file and the setup folder. Queries are automatically added as you modify the database schema. Naturally you can also manually add queries, or specify a custom PHP script to run in that upgrade step.
  16. Login Handlers are the different methods for logging into the IPS Social Suite. We currently support: "Internal", which is for accounts created natively through the suite. Facebook Twitter Microsoft (this is currently referred to as "Windows Live", though they rebranded to "Microsoft Account" a short while ago) LDAP "IPS Connect", which is our SSO solution for connecting your site with other IPS Social Suite installations or third-party applications. A generic handler for any MySQL database you have access to. In 4.0 we've made a number of changes to the Login Handlers which I wanted to mention. Improved Password Encryption We currently use a salted md5 hash for hashing passwords. md5 has been a popular password hashing technique for years - however, it is not the most secure hashing method. md5 is designed to be computationally efficient (meaning generating a hash is quick). The problem with this is that if a server were ever compromised to the point that someone were able to gain access to a database containing passwords hashed using md5, and someone were to use a program to generate and hash different strings repeatedly until a match were found, the password could be worked out. One particularly well-known program claims to be able to make 5.6 billion md5 hashes per second with a relatively modern GPU. Even with our hashing method which includes multi-level hashing and a salt, this means, assuming an 8-character long password using only alphanumeric characters were used, a password could be calculated in about 3 days. While I'm unaware of any cases of this actually happening, we want to make sure that our products are as secure as they can be. For this reason, in 4.0, we're migrating to Blowfish. Blowfish is a more cryptographically secure technique for generating hashes that is deliberately slow, meaning that even if your database were ever compromised, the passwords will still be secure. New Login Handlers In addition to the Login Handlers mentioned above, we've added support for Google and LinkedIn. Improved Facebook and Twitter support Currently, although you can log in with Facebook and Twitter, they're not treated on the back-end as true Login Handers. This is because of how Login Handlers in 3.x were designed (which was before such 3rd party login services were popular) in that they assumed you would provide a username (or email) and password directly into a form, and subsequently didn't accommodate the OAuth-style login processes. Since we've rewritten the way Login Handlers are designed, this means we can treat Facebook and Twitter (and Google and LinkedIn which both also use OAuth) exactly the same as the rest. Practically, this means you'll see Facebook and Twitter in the Login Handlers section of the Admin CP, and manage them as you would any other login method. Updated Microsoft Support Microsoft now support OAuth for login through them so we've updated to use that. In addition to being necessary for when they stop supporting the old way, it's much easier to set up for the administrator.
  17. Mark

    4.0 - Tables

    There's a table in the Admin CP of the IPS Social Suite that I really like - the members table. It has some really cool options - you can reorder the data just by clicking on a column head; you can quickly search for a member by typing a name into a search box at the top; there's some filter options to quickly show banned, locked, spam and validating members; and there's an advanced search form to search for members based on practically any criteria. It would be great if these features were available elsewhere. So much like we did for forms, we decided to create a central helper class for building tables. To demonstrate how it works, I'm going to go through, step by step, how I recreated the Admin CP members table in IPS 4. It starts with one line to create the table, and another to pass it to the output class: /* Create the table */ $table = new IPSHelpersTableDb( 'core_members', 'app=core&module=members&section=members' ); /* Display */ IPSOutput::i()->output = IPSOutput::i()->getTemplate( 'global' )->block( 'members', $table ); With just those two lines, you'll see this: Some things to note: We're calling IPSHelpersTableDb - the "Db" part indicates that the source of data for our table is a database table. There are other classes to use, for example, a JSON document as the data source. We pass it the name of our database table (or for the other classes, whatever the data source is) and the query string part of the URL where we're going to be displaying this (which we need to build the links and AJAX calls). I'm passing it to the output through a template called "block" which simply adds the dark-blue bar at the top, which isn't actually part of the table itself, and some padding. The "members" parameter is the key for the langauge string to use in that dark-blue bar. I'm passing $table directly to the template - the helper class has a __toString method which renders the table, so the output class thinks it's been given a normal string. The first obvious thing is that we're showing all the columns in the database table, which obviously we don't want. So let's add another line to specify which columns we want: $table->include = array( 'name', 'email', 'joined', 'member_group_id', 'ip_address' ); In this example, I'm giving the helper class a list of columns to include - I could alternatively pass a list of columns to exclude, if that would be more appropriate. The output is now this: Some things to note: It's worked out pagination itself. When you click a pagination link, the contents of the table will update with AJAX, including changing your browser's URL (unless you have JavaScript disabled of course, in which case it will work like a normal link). Pagination defaults to 25 results per page, but you can change that just by changing a property in the class. All the columns are clickable, which will resort the results. You can sort any column ascending or descending. Resorting will also update with AJAX (including changing your browser's URL), unless JavaScript is disabled. I want the headers to display something more meaningful than the column name. The system will automatically look for language strings which match the column name - you can also optionally specify a prefix, and it'll look for langauge strings which match that followed by the column name. Let's specify a prefix: $table->langPrefix = 'members_'; And I'll then create some language strings that match that (so "members_name", "members_email", etc.). The output is now this: Next - we need to change how we display some of those values. The joined date and the group are displaying the raw values from the database, but we want something more meaningful than that. To format the values, we simply create an array of lambda functions - one for each we want to format: $table->parsers = array( 'joined' => function( $val, $row ) { return IPSDateTime::ts( $val )->localeDate(); }, 'member_group_id' => function( $val, $row ) { return IPSMemberGroup::load( $val )->formattedName(); } ); I'm also going to add one additional line to specify the "main" column, which applies some additional styles: $table->mainColumn = 'name'; The output is now this: Some things to note: I'm using the IPSDateTime class to format the joined date. The ts method in this is a factory method which takes a UNIX timestamp and returns an object of IPSDateTime. IPSDateTime extends DateTime, so all the features of that class are available to us. The localeDate method returns a string with the date formatted appropriately according to user's locale. The IPSMemberGroup::load call being executed for each result may look like it might be resource intensive, but it caches objects it creates, so it's only actually "loading" each group once. Now I want to add a column with the user's photo. There isn't a single "photo" column in the database we can use for this (since the photo could be one they uploaded, a photo from their Facebook account if they're using Facebook Connect, a Gravatar image, or some other things), we need to use a method in the IPSMember class. This isn't a problem. I can simply add an element to our list of fields to include and add that into the parsers. $table->include = array( 'photo', 'name', 'email', 'joined', 'member_group_id', 'ip_address' ); $table->parsers = array( 'photo' => function( $val, $row ) { return IPSMember::constructFromData( $row )->photo('mini'); }, I'll also want to specify that we cannot use the photo column for sorting: $table->noSort = array( 'photo' ); The output is now this: Some things to note: Since this isn't a value which exists in the database, the value of $val in the lambda function will be NULL, however, $row has all the data for that record. We're not using IPSMember::load to get the member object, since that would execute an additional query for every result, which would be resource intensive, and unnecessary since we already have that data. Instead, we use the constructFromData method and pass it the row from the database. Next, I want to specify the default sorting. This is done with just two lines of code: $table->sortBy = $table->sortBy ?: 'joined'; $table->sortDirection = $table->sortDirection ?: 'desc'; The output is now this: Now, I want to add a quick search box. All we need to do is specify which column the quick search should look at: $table->quickSearch = 'name'; The output is now this: Some things to note: As you type, results are obtained with AJAX. You can page through your results (the number of pages will update automatically) and reorder your results by clicking the headers without loosing your search. I also want to allow more advanced search options - like to search by email address, or joined date. To do this, I create a new array: $table->advancedSearch = array( 'member_id' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'email' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'ip_address' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'member_group_id' => array( IPSHelpersTableSEARCH_SELECT, array( 'options' => $groups ), function( $val ) { return array( 'member_group_id=? OR ? IN(mgroup_others)', $val, $val ); } ), 'joined' => IPSHelpersTableSEARCH_DATE_RANGE, ); To explain what's going on here: The keys are the columns we're letting the user search on. The values are usually a constant indicating the type of search that is appropriate for that column. The member_group_id element is a bit more complicated. It has to specify an array of options (I've omitted the code to generate $groups in this snippet, but it'll be at the end of this blog entry), and, because we need to search both primary and secondary groups based on the value, there's a lambda function to get the proper WHERE clause for the query. Now, next to the quick search box there's a button which will bring up a modal popup (or just take you to a new page if JavaScript is disabled) which looks like this: Some things to note:The date entry boxes use the HTML5 date input type: If your browser doesn't support that, there's a JavaScript fallback: And if you're really awkward and are using a browser that doesn't support the HTML5 date input type and have JavaScript disabled, you'll see a regular text box where you can enter a date in practically any format, and it'll work it out. After performing the search, you can reorder your results by clicking the headers without loosing your search. Now, I want to add some filters so you can quickly see banned, spam, locked and validating members. To do this, you create an array simply specifying the WHERE clause to use in the query for each filter: /* Filters */ $table->filters = array( 'members_filter_banned' => 'member_banned=1', 'members_filter_locked' => 'failed_login_count>=' . (int) IPSSettings::i()->ipb_bruteforce_attempts, 'members_filter_spam' => '(members_bitoptions & ' . IPSMember::$bitOptions['bw_is_spammer'] . ') != 0', 'members_filter_validating' => 'v.lost_pass=0 AND v.vid IS NOT NULL' ); For this though, I'll also need to join the core_validating database table, so we add one more line for that: $table->joins = array( array( 'from' => array( 'core_validating' => 'v' ), 'where' => 'v.member_id=_0.member_id' ) ); The output is now this: Some things to note: The helper class will add the "All" filter automatically. It's getting the word to use for the filter by looking for a language string with the same key as the key in the array passed. Like everything else, clicking a filter updates the results with AJAX and the filter is retained in searches. Finally, the last thing I need to do is add a column with some buttons. You can specify a normal array for buttons to show in the header, and a lambda functions to return an array for buttons to show for each row: $table->rootButtons = array( 'add' => array( 'icon' => array( 'icons/add.png', 'core' ), 'title' => 'members_add', 'link' => 'app=members&module=members&section=members&do=add', ) ); $table->rowButtons = function( $row ) { return array( 'edit' => array( 'icon' => array( 'icons/edit.png', 'core' ), 'title' => 'edit', 'link' => 'app=members&module=members&section=members&do=edit&id=' . $row['member_id'], ), 'delete' => array( 'icon' => array( 'icons/delete.png', 'core' ), 'title' => 'delete', 'link' => 'app=members&module=members&section=members&do=delete&id=' . $row['member_id'], 'class' => 'delete', ), ); }; Our finished table looks like this: And behaves like this: http://screencast.com/t/KMFq8zCE To recap, here's the code, in it's entirety to generate that table: /* Create the table */ $table = new IPSHelpersTableDb( 'core_members', 'app=core&module=members&section=members' ); $table->langPrefix = 'members_'; /* Columns we need */ $table->include = array( 'photo', 'name', 'email', 'joined', 'member_group_id', 'ip_address' ); $table->mainColumn = 'name'; $table->noSort = array( 'photo' ); /* Default sort options */ $table->sortBy = $table->sortBy ?: 'joined'; $table->sortDirection = $table->sortDirection ?: 'desc'; /* Filters */ $table->joins = array( array( 'from' => array( 'core_validating' => 'v' ), 'where' => 'v.member_id=_0.member_id' ) ); $table->filters = array( 'members_filter_banned' => 'member_banned=1', 'members_filter_locked' => 'failed_login_count>=' . (int) IPSSettings::i()->ipb_bruteforce_attempts, /*@todo*/ 'members_filter_spam' => '(members_bitoptions & ' . IPSMember::$bitOptions['bw_is_spammer'] . ') != 0', 'members_filter_validating' => 'v.lost_pass=0 AND v.vid IS NOT NULL' ); /* Groups for advanced filter (need to do it this way because array_merge renumbers the result */ $groups = array( '' => 'any_group' ); foreach ( IPSMemberGroup::groups() as $k => $v ) { $groups[ $k ] = $v; } /* Search */ $table->quickSearch = 'name'; $table->advancedSearch = array( 'member_id' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'email' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'ip_address' => IPSHelpersTableSEARCH_CONTAINS_TEXT, 'member_group_id' => array( IPSHelpersTableSEARCH_SELECT, array( 'options' => $groups ), function( $val ) { return array( 'member_group_id=? OR ? IN(mgroup_others)', $val, $val ); } ), 'joined' => IPSHelpersTableSEARCH_DATE_RANGE, ); /* Custom parsers */ $table->parsers = array( 'photo' => function( $val, $row ) { return IPSMember::constructFromData( $row )->photo('mini'); }, 'joined' => function( $val, $row ) { return IPSDateTime::ts( $val )->localeDate(); }, 'member_group_id' => function( $val, $row ) { return IPSMemberGroup::load( $val )->formattedName(); } ); /* Specify the buttons */ $table->rootButtons = array( 'add' => array( 'icon' => array( 'icons/add.png', 'core' ), 'title' => 'members_add', 'link' => 'app=members&module=members&section=members&do=add', ) ); $table->rowButtons = function( $row ) { return array( 'edit' => array( 'icon' => array( 'icons/edit.png', 'core' ), 'title' => 'edit', 'link' => 'app=members&module=members&section=members&do=edit&id=' . $row['member_id'], ), 'delete' => array( 'icon' => array( 'icons/delete.png', 'core' ), 'title' => 'delete', 'link' => 'app=members&module=members&section=members&do=delete&id=' . $row['member_id'], 'class' => 'delete', ), ); }; /* Display */ IPSOutput::i()->output = IPSOutput::i()->getTemplate( 'global' )->block( 'members', $table );
  18. When developing, modifying the database schema (such as adding a column to a table) can be surprisingly time consuming. Currently, we have to: Make the change locally Change the installer Add the query to make the change to the upgrader for whatever version we're working on Let the other developers know so they can run the query to make the change in their installs. This can cause issues, especially at the last step - we currently use a large .sql file in the trunk directory of our repository which we add queries to that we need everyone else to run - sometimes, one of us doesn't notice this has been updated. I'm sure also, third party modification authors are familiar with this annoyance. In 4.0, one of the things I really wanted to do was build a central "Developer Center" from which both us and modification authors can manage aspects of the application without digging into installer/upgrader files, XML files and manually copying and pasting things when creating a new module or extension. Part of this developer centre is a GUI for modifying the database schema. When you make changes, the changes will automatically be ran against your local database and added into the installer and upgrader. Internally, we'll also then have a special script which runs when we SVN update to copy these changes over when another developer makes a change. The first page in the Database Schema Management will be a list of tables: You can add a table to the list either by creating a new table, importing a table which is already in the database or uploading a .sql file containing a CREATE TABLE statement: When you edit a table, you can manage the columns, indexes and rows which are inserted by default: If you try to edit a table and your local database does not match what the schema has, you'll be shown the conflicts: This, plus the other features in the Developer Center, which we'll talk about in later blog entries, make the process of developing applications easier for both us, and third party authors. Fun fact: I'm writing this blog entry from the home of IPS in Lynchburg, VA rather than my usual office in the UK :smile:
  19. Mark

    4.0 - Forms

    Forms are an ubiquitous aspect of any web application. In the IPS Social Suite, particularly in the Admin CP, I often find myself copying and pasting code in various places to create a form. We've had the ipsRegistry::getClass('output')->formInput() and similar methods since 3.0, but you still have to copy all the HTML to display the rows, and write all the code to validate it yourself. Copying and pasting code is something all developers hate. It's a red flag that you're probably doing something wrong. In IPS 4.0, we've written a central form building helper class to alleviate this. Just as a reminder: Everything in this blog is a work in progress - naturally someone with a much keener design sense than I will be going over the interface - I'm just demonstrating the functionality, not a finished product :smile: The Basics Let's say I want to create a form with a single text input field. I simply initiate the form, add an object representing the input, and then display the form (it has a __toString method) - like so: $form = new IPSHelpersForm(); $form->add( new IPSHelpersFormText( 'name', 'default value' ) ); IPSOutput::i()->output .= $form; The helper will automatically look for language strings with the same name as the form element and use them. If there is a language string with the same name and then "_desc" - it'll use that as the description. So the above code, in the ACP, produces something like this: Required Let's say I want the field to be required - I just pass a third argument indicating so. When the form is submitted, if no value has been provided, it will automatically display an inline error: new IPSHelpersFormText( 'name', '', TRUE ) Options The fourth argument I can pass is an array with options dependent on the type of input field I'm adding. These options may change the way a field is displayed or add additional validation. So for a text field, I can specify the minimum and maximum length (which, naturally takes multibyte languages into consideration): new IPSHelpersFormText( 'name', '', TRUE, array( 'minLength' => 5 ) ) For a number field, I might specify the number of decimal points to round to, which it will do on the fly: new IPSHelpersFormNumber( 'name', 0, TRUE, array( 'decimals' => 2 ) ) Watch Video Or I could add an "Unlimited" checkbox, which is quite common for Admin CP settings: new IPSHelpersFormNumber( 'name', 0, TRUE, array( 'unlimited' => TRUE ) ) Custom Validation If the built-in options don't provide enough validation for a given need, you can pass a lambda function as a 5th argument: new IPSHelpersFormText( 'name', '', TRUE, array(), function( $val ) { if ( $val === 'Bad Value' ) { throw new IPSHelpersFormException( 'That value is not allowed.' ); } } ) Uploads Of course - simple input fields aren't all that can be done. How does drag and drop uploading sound? new IPSHelpersFormUpload( 'name', NULL, FALSE, array( 'multiple' => TRUE ) ) Watch Video (By the way, you'll notice a stripy bar flashes up briefly - this is a real progress bar, but because I'm uploading to localhost it's filling faster than it displays - in practice, you'll see a nice smooth-filling progress bar) Getting the values The form helper will by default (you can override it of course) submit to the same page it's on. You can check if it has been submitted (and passed validation) and obtain the values if so simply by calling the values() method - here's an example: $form = new IPSHelpersForm(); $form->add( ... ); if ( $values = $form->values() ) { // $values contains the values from the form } else { IPSOutput::i()->output .= $form; } The values will be returned in a way that is appropriate to the input type. For example, a text input field will return a string, number input will return either an integer or float, an upload field will return an IPSFile object (or array of them, if you're accepting multiple files), etc.
  20. In 4.0, we have made changes to the database class to make use of prepared statements. For insert and update queries, the syntax is the same as it always has been: IPSDb::i()->insert( 'table', array( 'foo' => 'bar' ) ); However, where previously the database class would try to work out the type of variable passed to it - it now binds these to a prepared statement. The real usefulness of this change though, is apparent when you need to use a where clause. Where previously you'd have to do something like this: $this->DB->buildAndFetch( array( 'select' => '*', 'from' => 'table', 'where' => "foo='" . $this->DB->addSlashes( $foo ) . "'" ) ); You can now do: IPSDb::i()->buildAndFetch( array( 'select' => '*', 'from' => 'table', 'where' => array( 'foo=?', $foo ) ) ); We calculate the datatype based on the variable datatype, so previously where you had to do things like $this->DB->setDataType( 'foo', 'string' ) when you wanted to store a value like '01' - ew) - you can now just cast the variable to whatever datatype you like. For example, if you wanted to ensure that the variable was cast as a string to avoid issues where a user name of '007' was detected as an integer and converted to '7' then you'd use: IPSDb::i()->buildAndFetch( array( 'select' => '*', 'from' => 'table', 'where' => array( 'foo=?', (string) $foo ) ) ); Not only is this easier to type and to read, it ensures that the database class always takes care of escaping things properly.
  21. A while back, we casually mentioned in a blog entry that 4.0 would be next major version after 3.4. Development of 4.0 is underway and we're going to be using this new blog to talk about development as we go. As Brandon mentioned a couple of days ago - the format of these entries is going to be developer-specific. If what we're saying doesn't make much sense right now, we will still be putting announcements up in our main blog when they're finished and ready for everyone to see. Because of that, it's also worth bearing in mind that everything is subject to change. I'm going to be posting code samples, screenshots and so forth - but everything in this blog is a work in progress - not the final product - and that will probably show. With that out the way - let's talk about 4.0! :D The file structure Currently, applications are mostly self contained in their folders (which is either /admin/applications, /admin/applications_addon/ips or /admin/applications_addon/other) however, other files are dotted around in /interface, /public, etc. In 4.0, applications will be completely self-contained within a single /applications directory. An application directory will look something like this: extensions dev css html img js lang admin front (it's "front" rather than "public" now) [*]interface [*]modules[*]setup [*]sources [*]tasks [*]xml You will notice the inclusion of a /dev folder. This will not actually be shipped in production, but rather replaces /cache/lang_cache/master_lang and so forth in the 3.x line. Outside of the applications directory, there will be a "system" directory, which contains core framework classes. Namespaces and autoloading In 4.0, we'll be making use of PHP namespaces and using an autoloader. The best way to demonstrate how this works is with a few examples:classDb (/ips_kernel/classDb.php) is now IPSDb and located in /system/Db/Db.php output (/admin/sources/classes/output/publicOutput.php) is now IPSOutput and located in /system/Output/Output.php class_forums (/admin/applications/forums/sources/classes/forums/class_forums.php) is now IPSforumsForum and located in /applications/forums/sources/Forum/Forum.php IPSDispatcherFront and IPSDispatcherAdmin are two new classes (with similar functionality to ipsController in 3.x) and both extend IPSDispatcherDispatcher - all 3 are located in /system/Dispatcher/ in individual files. Better framework design Where appropriate, classes are being refactored to make better use of appropriate design patterns. One lovely side-effect of this is ipsRegistry no longer exists. Instead of, for example ipsRegistry::DB() you now use IPSDb::i() - the Db class uses a multiton design pattern (I didn't pass any arguments in that example, which doing will cause the Db class to load conf_global.php and create the default database connection, but I could have passed it a key) - the i method in this case will create the database connection if it doesn't already exist. To give another example - IPSMember (the new IPSMember) uses an Active Record pattern. So there's no more of this: IPSMember::isInGroup( 1, 4 ); It's now, the much more logical: IPSMember::load( 1 )->isInGroup( 4 ); Monkey patching hooks One of the great things about the IPS Community Suite is hooks - you can easily create a class and instruct the framework to use that instead of a core class. Now, I don't know about you, but I really, really, really hate having to do this: $class = IPSLib::loadLibrary( '/path/to/file', 'myClass' ); $object = new $class; Especially if what you want to call is a static method, in which case it can't be done. You want to of course, just be able to do: $object = new myClass; or: myClass::myStaticMethod(); There is a concept in software engineering to do this sort of thing, called monkey patching, and by clever use of the autoloader, we've managed to make this work. loadLibary and loadActionOverloader are no more. These points are of course, just the beginning of 4.0. Stay tuned for more :smile:
  22. Mark

    Auto-Upgrader

    Last year, we added an auto-install/upgrade tool into our client area. The tool would upload the latest version of the IPS Community Suite to your server automatically. The way it worked was after submitting the request, you were entered into a queue, and a program on our server uploaded the files, and emailed you when it was done so that you could run the upgrader script. It normally took about 30 minutes. 30 minutes is pretty good. For someone without the technical knowledge to upgrade, it's a great alternative to asking for a support agent to do the upgrade for you (especially just after a release when everyone is doing just that). However, many users still prefer to do the upgrade themselves (after all, if you can download the source files and upload them to your server in the same time, what's the point?). We decided we should try and get that time down. We also wanted the process to be much more seamless - so you click "Upgrade", get a loading screen, and then are taken immediately to the upgrade script when it's done, rather than waiting for an email. Interested to see how fast we managed to get it? Take a look for yourself: Watch Video In addition, the system can now handle communities that have renamed their admin directory seamlessly without issue (it previously reset to /admin), and the system will automatically detect which encoding of IP.Nexus to use for your server, if applicable (previously it would only use Zend). We really hope the Auto-Upgrader will provide a much easier way for you to keep your community up-to-date with the latest features and enhancements. If you've not already upgraded to IP.Board 3.4, why not go and try it out now? To access the tool, simply go to the Purchases area of the client area, select the community you want to upgrade and click the big "Upgrade Now" button.
  23. As we wrap up principle development of new features for IP.Board 3.4, I wanted to go through some of the other changes we've implemented for the new release. Share a single post A common feature request has been the ability to share a single post. You might want to share a great reply to a topic on Facebook or perhaps email a link to an interesting post to a friend. You can now do this by clicking either the post number or the little share icon to the right of the post number. This brings up a new modal window System Templates Some "system templates" such as the wrapper used for HTML emails were previously only editable by manually changing the files which contained those templates. This is inconvenient, especially as it means one has to remember not to upload those files when upgrading IP.Board. In IP.Board 3.4, these templates will be editable from the Admin CP making it much easier to maintain edited versions of these templates. Mobile Moderation Our mobile theme is suitable for all mobile devices. Even though we have our mobile application available for the iPhone and iPad, we want to ensure that other devices are able to interact with IP.Board. A very common request has been to enable basic moderation actions from within the mobile theme. To that end, we've added buttons for common topic moderation into the mobile skin. Editor Pasting A lot of the time you find that you just want to paste plain text into the editor so you don't have to then remove formatting such as background and font colors. We've made this an option from within the IP.Board 3.4 editor via the new Options icon (far right on the screen shot). You can still paste as rich text by clicking on the relevant paste button on the top right section of the toolbar. We hoped that you've enjoyed reading this series of blogs on IP.Board 3.4. We're currently putting the finishing touches to this major new release and can't wait make it available for release!
  24. IP.Board has for a long time allowed administrators to send bulk mails to members, including the ability to filter recipients and use variables to customise the message sent to each member. This is an important tool for communicating with the members of your community and in 3.4 we've made it even better. The problem with bulk mail Sending vast amounts of email through your own server is troublesome. Firstly, it takes a long time (you can't just send out thousands of emails in one go) and due to the way PHP works, you need to have activity on your community to initiate the sending of each batch of emails (you could set up a cron, but you'd probably only want it running when you actually have a bulk mail sending, and they're fiddly to set up). Secondly, if you're on shared hosting, other sites on the same server may have given your server a bad reputation and caused it to be placed on blacklists, this causes the emails you send to be more likely to be marked as junk. Thirdly, sending vast amounts of email through your own server is an expensive task - most communities we see use the same server for sending emails as hosting the community itself, meaning resources are being used for the sending of those emails and not serving your users. Fortunately, these problems are well-known throughout the internet and a number of companies offer services to send mail for you, through their servers to alleviate these problems. In addition, these services provide web applications where you can view statistics, and track how many of your emails have been opened, rejected, etc. We're really pleased to announce that in IP.Board 3.4 we've built in integration with Mandrill, a service of well-known and respected MailChimp. How it works Setting up integration with Mandrill is really easy. In the new "Community Enhancements" section of the Admin CP (which we've mentioned in earlier blog entries), there will be an option for Mandrill: After creating an account on their site, all you do is enter your account details: IP.Board will from then on send all bulk mails through the Mandrill service. And it's not just bulk mails. You can even configure IP.Board's normal email settings to send all outgoing emails via Mandrill's SMTP server. Sending bulk mails As part of our improvements, we've also: Tidied up the interface for sending bulk mails Improved the unsubscribe link sent in emails to be a one-click link (rather than requiring users sign in and uncheck the box) Built extension capabilities the filter options available when composing a bulk mail, meaning 3rd party applications can add their own filter options (we'll use this for example, in a future version of IP.Nexus to allow you to send bulk mails to anyone who's purchased a particular item). Added a tab on the confirmation screen to allow you to view the list of recipients before actually sending the bulk mail. All these improvements are present even if you choose not to use the new Mandrill integration. Statistics and Tracking You can view statistics via the Mandrill web application. Emails sent from IP.Board's bulk mail system automatically enable tracking for opens and clicks. They even have iPhone and Android apps available. Pricing Full pricing details are available from Mandrill - for up to 12,000 emails per month though, the service is completely free.
  25. A Content Delivery Network, or CDN is a distributed system of servers to provide high-availablity and high-performance. For example, rather than serving your CSS, images and Javascript from the same webserver that PHP and the rest of your community runs on, these are loaded from a network of servers, improving performance and reducing load on your server. Having a CDN can improve your community's quality, reliability and scalability, as well as reduce your hosting costs. By offloading the serving of images, CSS, Javascript, etc. you free up your system resources to serve the real content which makes for a better experience. Some say that there are also SEO advantages o CDN use. The IPS Community Suite has supported Content Delivery Networks for some time. However, they can be expensive and difficult to set up. Many clients want a CDN but do not know where to go and we hope to help them out. We are pleased to announce a new service, IPS CDN, which will allow you to quickly, easily and inexpensively start using this important technology on your community. How does it work? The IPS CDN service will be supported in IP.Board 3.4. After upgrading, you'll notice a new section of the Admin Control Panel called "Community Enhancements" - one of the options available here is "IPS CDN". On this page you'll be able to enable the CDN, which will take you to a new page in the client area where you can purchase credits: Note that the packages shown in this screenshot are examples only. As you will notice from that screenshot, you can purchase credits for your CDN account as and when you want, and optionally set up your account to automatically top-up as you run low on credit. Once this is done, your community will automatically start using the CDN service. If you do not set up automatic top-up, we'll send you an email when your account is running low on credit (when you go below 10GB, 5GB and 1GB). If your credit runs out without you topping up, your community will automatically stop using the CDN service - there will be no interruption to the running of your community, it will automatically notice there's no credit remaining and go back to serving resources locally. If you top up again, it will automatically enable itself again. One problem of using the IPS Community Suite (or indeed any application) with a CDN is that when a resource is changed locally, the CDN needs to be recached to notice the new changes. Unless the CDN is notified of a change it will keep serving the old copy of the file - sometimes for up to 24 hours. This can cause much confusion to you and your visitors. To remedy that problem: since editing skins and the CSS is done from the Admin CP, the system will automatically call the CDN service to recache resources as and when you change them, so you don't need to worry about this. You can keep track of your usage in the Admin CP, where you'll be able to see a graph with your usage over the last 7 days. You'll also be able to buy more credits, disable the system, and manually recache. How much does it cost? The service will be based on "Pay As You Go" pricing. Meaning there's no minimum sign up fee - you simply purchase credit on your account and that credit is good for an amount of data transferred through the service. As you use the CDN, your credit will decrease and you can top up with more. The base price for the CDN will be at or around $0.18 per GB of bandwidth served through the CDN and may drop as you reach higher levels. We realize this is rather unspecific right now but we are negotiating with CDN providers to get the best bulk pricing. It will be our goal to pass any volume savings we receive on to our clients as we look at this service is a great way to enhance our client's experience and want to encourage its use. Everyone will however be offered their first Gigabyte for free to try out the service before purchase. Of course if you do not want to use the IPS CDN service and want to use another provider you can certainly do that. We believe the click and go setup for the IPS CDN service will encourage usage and benefit all IPS Community Suite users. Future CDN Integration Right now the CDN is a basic "pull" implementation whereby it simply pulls data from your live server then serves it via its cache. In the future we hope to implement storage services. This would mean that uploaded files would be stored on the CDN rather than your local computer. This is great to reduce storage costs on your hosting and also means that, other than the actual processing of data, your community's files are geographically distributed. For our power users this would also mean even easier cluster/cloud hosting. Storage and other CDN integrations will come in future versions of the IPS Community Suite so for now enjoy the current features while we work on even more great additions!
×
×
  • Create New...