The architecture of ringo is shown below.
Ringo is a Pyramid based application which can be extended to build your own applications. This is a layered architecture where Pyramid brings in the basic functionality like session handling and handling requests and responses in web applications. Ringo sits on top of Pyramid and provides commonly used functionality often used in modern web applications. See Features for an overview.
Note
Ringo is also a standalone application. You do not need to build another application on top of ringo to get something working to get an impression of ringo or start developing. Ringo is under steady development. This is a benefit if you use Ringo as base for your application. Your application will get bug fixes, new features or improved functionality in most cases by simply updating Ringo to a new version.
Ringo itself uses some external libraries to provide some of its functionality. E.g the formbar library is used to build all forms and do validation. The access to the database is done with the ORM SQLAlchemy.
A ringo based application is another pyramid based application which basically extends ringo. See Exending An Existing Pyramid Application for more details on how this is done.
All this magic is already done in a pyramid scaffold which comes with ringo. Using this scaffold will create an application which uses ringo functionality by simply importing it at the right places and take care the basic configuration is done in the proper way. See Develop/Create on a Ringo based application for information on how to create an application using this scaffold.
The term “Module” is central and often used in ringo. Therefore it is important to understand what a module is. This section tries to explain that.
You can think of a module in the way that it provides the infrastructure to work with a certain type of data in a web application. Where certain type of data means users, files, movies etc. rather than integers or datevalues. Lets call them items from now on.
A module will have a model for the items you want to work with. It will include views to handle incoming request and generating the proper responses for users. Further it has templates which define how the pages in the application will look like. Finally there are configuration files to define how the forms and overview tables will look like.
Ringo already come with many modules. One module per item. There is a module for the user management, a module for appointment and so on. There is also a module to handle the modules itself. So we can say in general: ringos functionality is the sum of all modules functionality. Ringo or a ringo based application can be extended by adding new modules. Fortunately you will not need to create this infrastructure for you own. See Add a new modul to your application for more information.
Each module has a common set of configuration options which can be done directly in the web interface. See the modules entry in the administion menu for more information.
Modules provide actions which can be used to manipulate the item of a module. Ringo provides some basic CRUD [1] actions which are available on default for every module.
Every time you want to do something different to your items you will likely want to implement another action for the module. See Adding a single custom actions to a module for more details how to add actions to a module.
[1] | CRUD means: Create, Read, Update, Delete |
Ringo is organised in the following file layout:
.
├── alembic
├── docs
└── ringo
├── lib
├── locale
├── model
├── scaffolds
├── scripts
├── static
│ ├── bootstrap
│ ├── css
│ ├── images
│ └── js
├── templates
│ ├── auth
│ ├── default
│ ├── internal
│ ├── mails
│ └── users
├── tests
└── views
├── forms
└── tables
Below you see the basic schema of the ringo database. The schema only lists some central tables which help to understand how the things are wired together in ringo. The table colored orange example table in the top left is an example for any other table which will store items of a modul. Every item of an item has a reference to the module it belogs to. Further the current example has a reference to a user and a usergroup which defines the ownership [2] per item is is important for the authorisation.
Let us begin with the modules table. This table will store information on all available modules in the system. It basically stores the configuration per modul. As described in the Modules section each modul has (Modul actions) which are stored in the actions table. The NM-table nm_actions_roles define which roles are allowed to use the actions in the module. See Permission System for more information on how the Authorisation is implemented in ringo.
The users table stores all the users in ringo. The users table only holds minimal data which is required for authentification and authorisation. Additional information like the name, email etc. is stored in the profiles table. Every user has a profile.
Users can be organised in groups using the nm_user_groups table. All usergroups are stored in the usergroups table. Roles can be assigned to usergroups and users. This is done with the NM-table nm_user_roles and nm_usergroup_roles.
The table user_settings and password_reset_request are helper tables to save user settings like saved search queries or store the tokens to trigger the password reset.
[2] | The ownership feature can be added by using the Owned mixin. |
Mixins can be used to add certain functionality to the items of a module. Mixins are used in multiple inheritance. The mixin ensures that the item will have all needed fields in the database and provides the proper interface to use the added functionality. Example:
class Comment(BaseItem, Nested, Meta, Owned, Base):
__tablename__ = 'comments'
_modul_id = 99
id = sa.Column(sa.Integer, primary_key=True)
comment = sa.Column('comment', sa.Text)
...
The comment class in the example only defines the two fields id and comment. But as it inherits from Nested, Meta and Owned it also will have date fields with the creation and date of the last update, references to the user and group which ownes the Comment. Further the ‘Nested’ mixin will ensure the comments can reference each other to be able to build a hierarchy structure (e.g Threads in the example of the comments).
Important
As most of the mixins will add additional tables and database fields to your item it is needed to migrate your database to the new model. See alembic_migration section for more information.
Mixin to add a created and a updated datefield to items. The updated datefield will be updated on every update of the item with the datetime of the update.
Mixin to add references to a user and a usergroup. This references are used to build some kind of ownership of the item. The ownership is used from the permission system.
It is posible to configure inhertinace of the owner and group from a given parent element. This information is used only while creating new instances of the modul. If configured, the default group and owner information will be overwritten. This is done at the very end of the creation process. See ‘’save’’ method of the BaseItem. You can configure the inhertiance by setting the name of the relation to the parent item in the ‘’_inherit_gid’’ and ‘’_inherit_uid’’ class variable.
Mixin to make nested (self-reference) Items possible. Each item can have a parent item and many children. The class will add two relation attribute to the inheriting class. The parent item is available under the parent attribute. The children items are available under the children attribute.
Mixin to add logging functionallity to a modul. Adding this Mixin the item of a modul will have a “logs” relationship containing all the log entries for this item. Log entries can be created automatically by the system or may be created manual by the user. Manual log entries. Needs to be configured (Permissions)
Mixin to add version functionallity to a modul. This mixin is used to store different “versions” of an item. On each update of the item, the serialized values of the item will be saved in the version table. Item modul will have a “versions” relationship containing all versions of the values for this item.
Mixin to add one or more Statemachines to an item. The statemachines are stored in a internal ‘_statemachines’ dictionary. The current state is stored as integer value per item. This field must be created manually. The name of the field which stores the value for the current state must be the keyname of the ‘_statemachines’ dictionary.
Example Mixin usage:
class FooMixin(StateMixin):
# Mixin inherited from the StateMixin to add the Foobar
# state machine
# Attach the statemachines to an internal dictionary
_statemachines = {'foo_state_id': FooStatemachine}
# Configue a field in the model which saves the current
# state per state machine
foo_state_id = sa.Column(sa.Integer, default=1)
# Optional. Create a property to access the statemachine
# like an attribute. This gets usefull if you want to access
# the state in overview lists.
@property
def foo_state(self):
state = self.get_statemachine('foo_state_id')
return state.get_state()
Mixin to add comment functionallity to a modul. Adding this Mixin the item of a modul will have a “comments” relationship containing all the comment entries for this item.
Ringo offers the option to equip items of the modules with a one or more state machines to model statefull workflows.
An classical example for a statefull workflow is a process where an item can be in an draft, review and published state.
The statemachine offers features like
- Restrictions on access
- Conditional transitions
- Handlers.
Depending on the state the state machine can be configured to restrict access to the item for certain users. Further you can define conditions which must be true before the state can switch into another. Finally you can write certain handlers which are called right after the state has changed.
A state machine can be attached to the items using a StateMixin which organises the state machines an provides a unique interface.
For detailed descriptione of the involved classes see API documentation of Statemachine
The permission system addresses two basic questions:
To answer these two questions the permission system of Ringo is a combination of concepts of the permission system known from the Unix file system and a roles based permission system.
The Unix file system part answers the first question: Who is allowed? Therefor every item in the system inherited from the Owned stores information to which owner and which group it belongs to. Only the owner, members of the group or users with an administrational role are granted access to the item in general.
After the permission to access the item in general is allowed, the role bases system answers the second question: What is allowed. The permission system will now check which Roles the users have and which actions are allowed for these roles.
There are currently two ways a user can be equiped with permissions:
1. If the user is the owner of the item, then all permissions of the users roles will be applied.
2. If the user is member of the items group, then the permissins of the groups will be applied.
Note
Currently there is no anonymous access to the item. See Issue61 in the ringo bugtracker. A workaround might be to setup a user group with all users of the system and assing the needed roles to it. Then set this group as the item group.
See Authorisation for more details on this.
Authentication is done once in the login process. If the user has logged in successful an auth cookie is saved. From then on the user object is loaded from the database on every request with the roles and groups attached to the user. This user object is used later for the Authorisation. If the user is not logged in the user object is empty.
The authentification has a default timeout of 30min. The timeout will be reset after every new request of the user. The timeout can be configured in the application configuration bei setting the ‘auth.timeout’ config variable.
The permission system in Ringo uses the Pyramid Pyramid Authorisation and Authenfication API
Authorisation is done on every request. The authorisation will check if the user is allowed to access the requested resource.
A resource is an url or an item which is accessed by calling the url in your application. In all cases this resource is build from a resource factory for every request. Every resource will have an ACL which determines if the user of the current request (See Authentification) is allowed to access the resource.
Ringo’s part in the authorisation process is to build the ACL. This ACL is then used by the Pyramid security API. Therefor ringo implements helper functions to build ACL lists which model the ringo permission system.
See Adding Authorization tutorial for more information how things work in general under the hood.
See Security for documentation on helper functions used to build the ACL.
A Role s used to configure which actions are permitted to users. Therefor each role will have an internal list of modul actions. A user will be allowed to call all actions assigned to the role he is equiped with.
Each class can implement one of the following event handlers to realize automatic modifications of the items:
- create_event_handler(request, item, **kwargs)
- update_event_handler(request, item, **kwargs)
- delete_event_handler(request, item, **kwargs)
The functions will be called for all base classes of the item automatically in the base controller if the specific view function for the event (update, create, delete) is excecuted.
Some of the Mixin classes do already have some predefined event_handlers configured.