views
With Views now in Drupal core as of D8 the two most common basic contrib modules from previous versions of Drupal, CCK and Views, are now both part of core.
CCK, now called Entities, from one perspective is what allows us to create arbitrary database tables through a web interface where we can hold arbitrary data yet work with it through a common interface, and use Drupal goodies like widgets, theming, content creation forms and pages, etc. on them. Views is the complement to this way of creating custom content. Views in core means we can now retrieve or query that arbitrary, custom content in arbitrary and custom ways.
Views is basically a web UI for an SQL query builder. The fields are the SELECTed database columns and the filters are your WHERE clause.
This isn't just a feature for non-technical dudes and non-developers. Just because you can write SQL queries doesn't mean you should write all of them. Views gives you an interface into various Drupal core and contributed modules and the content they manage, if those modules integrate with Views via hooks. Entities (or CCK) can create a mess of database tables. Sometimes these tables will change structure from under your feet due to some changes via the admin interface. You might not know until your code is broken. But using Views, the query will always be up to date.
Now that Views is in core, other parts of core could possibly depend on and build on Views, just like many other parts of Drupal core use concepts that came from Entities (CCK). So Drupal core can do a lot more out of the box. And contributed modules can be written to depend on only Entity and Views, which are part of core, and so these new modules won't have any other 3rd party dependencies. That's a win for module developers.
- tomo's blog
- Login to post comments
- Comments
Say you have a bunch of blocks and you want them to be displayed on certain nodes of varying content type based on some criteria like the content type and some CCK fields or taxonomy. You can't do this with the stock block visibility settings without writing custom PHP code.
But we can implement it using some existing basic Drupal building blocks: CCK and Views
1. Create a content type called Visibility Block.
You might have a field for content type where the possible values are returned from code which returns an array of the content types (using function node_get_types()).
2. Then for any fields you want to match, you'll have the same fields in this content type. For example, if one of your content types has a textfield and the possible values are 1, 2, 3, then do the same for Visibility Block.
When you create a Visibility Block, you'll have your block content in the body as normal (optionally you could use Block Reference and create blocks like usual and then link to them in the node instead, but I see no point in the extra effort and redirection), then select the conditions for the pseudo-block being visible.
3. Now create a view called Visibility Blocks Viewed. You'll create block displays, one for each content type that you have a Visibility Block set for which may only be one or two of your c-types.
Create an overridden argument each block display for the content type -field- in Visibility Block. You want to match the content type of the viewed node with the field in the Visibility Block, which are not the same type of thing. So you'll need to convert the argument in code.
You'll call menu_get_object() to get the $node because it's better than "$node = node_load(arg(1));". For the chosen c-type for that block display you will check that the implied node's c-type is what ever type you want to show in this block display because you will also check any fields that are specific to this c-type. Use PHP to supply a value since no argument will be passed in, and have the code load the current node and return the type. Then use PHP for the Validator Options and if the c-type doesn't match then you will display empty text. If you have multiple c-types which share CCK fields then you can put them into a single block display.
4. For each content type, in the chosen block display you will create a new argument for each compared against CCK field. You will pick a field from V-block and then use PHP to return a default value of the currently viewed node's field's value. You won't need to do Validation on these arguments.
5. Finally, you may want to limit the number of nodes returned, maybe just one. Now give this block a title and save it and configure its region in the normal block admin.
--
Why not the normal block visibility settings?
Because there you can't even configure by content type (anymore in D6). You can configure by path glob and by PHP code which overrides the path glob field (including in the database).
So under "Page specific visibility settings", set to "Show if the following PHP code returns TRUE (PHP-mode, experts only).", the field is blocks.pages. Essentially, PHP code works by overriding the pages list.
Why not use Block Page Visibility?
Block Page Visibility (http://drupal.org/project/bpv) enables site developers to centralize the display of blocks to a single PHP function. It is an alternative to controlling display via each block's configure form. The more "sometimes on, sometimes off blocks" that a site uses, the more useful this module becomes.
This takes over the visibility settings of all your blocks by calling:
$sql = 'UPDATE blocks SET visibility = 2, pages = CONCAT("<", "?", "php ", "return bpv_is_visible(\'", module, "-", delta, "\'); ", "?", ">") WHERE status=1 AND theme=\'%s\'';
Sometimes it's easier to configure a block by just whether you're logged in, or whether you're on the front page.
This module doesn't give you any finer grained controls. And you have to configure every block in code. You also lose all your current block visibility settings once you install this module. To use this module you have to implement your own bpv_config or bpv_configuration (I think it's a bug that it's looking for bpv_configuration but actually uses bpv_config).
- tomo's blog
- Login to post comments
- Comments
Websites, including Drupal sites, often need Frequently Asked Questions and answers to them. Drupal, as a content management system, should manage your question and answer content intelligently. With all the great modules contributed to Drupal's community you might think there are some good FAQ modules. In my experience, the Drupal FAQ module is too rigid, and therefore unusable for most of my sites. But we can build a FAQ system using basic Drupal building blocks.
What we need: taxonomy, blocks, views, a few lines of PHP (that can be stored in the database - no custom module required)
1. Content type: Create a new content type. CCK is optional here, as you can just use Title as Question and Body as Answer.
2. Vocabulary: Create a new vocabulary called FAQ. Add a few terms if only for testing.
3. Devel Generate: Optional - requires Devel module's devel_generate to generate some test nodes with test questions, answers, and topics. devel_generate can be run from the command line too if you have drush installed - just run "drush genc". Anyways, generate a few dozen nodes, as many nodes as questions you have. It'll be easier to mass edit the questions once the nodes have already been generated.
4. Views: You'll need two views although they can also be two displays of a single view, so let's do that.
a. Create a view, filtered by your FAQ content type (and published or published/admin).
You will add three node fields:
Node: Nid (Nid) [make this field hidden, but its value is used in the rewritten Question field below]
Node: Title (Question)
Node: Body (Answer)
For the Question, rewrite the output to:
<a name="q-[nid]"></a> [title]
You'll have on argument, which is the Term (FAQ vocabulary topic) in the URL.
- Configure Argument Taxonomy: Term
-- Provide default argument
--- Default argument type: Taxonomy Term ID from URL
-- Validator: (Choose your FAQ vocabulary)
-- Argument type: Term name or synonym
-- (Optional) Transform spaces to dashes in URL
Now turn this default view into a page that's not overridden in any way. Set the URL to be something that's NOT your vocabulary name because your URL will conflict with the default taxonomy paths ("taxonomy/term/%" - which Taxonomy sets up path aliases for from each vocabulary with each term - but that page may also be being overridden by a view included by Views by default). So if you want your URLs to be like "/faq/return-policy" then name your Vocabulary like "FAQ Terms" instead of "FAQ".
At this point you should save your new view and be able to go to "/faq/troubleshooting" or whatever. It will give you a list of questions with answers. But usually a FAQ section will also list out all questions at the top with links to answers below. How can we accomplish that?
b. Let's create a new Display that's a Block. Now override the fields and remove the Answer field. Override the display of the Question to:
<a href="#q-[nid]">[title]</a>
Optionally, set a blank title for this field. Then we will have a block that is just a list of questions. But the argument won't work anymore since it's a block. So you need to override the argument "Taxonomy: Term".
- Default argument type:
-- PHP Code
--- return arg(1);
- Validator: Basic validation + Transform dashes in URL to spaces in term name arguments
This will find the term "charity" in the path "/faq/charity" and pass it on.
5. Now that the block is created, you need to make it display. We want to display it on the top of our first view!
This is assuming you have a region in your theme for blocks at the top of content in pages. You could choose another location but it should really appear as the first content you see. But you want this block to appear ONLY on this page! So in block admin, configure the block you just created, and under:
Page specific visibility settings
- Show block on specific pages:
-- [check] Show on only the listed pages.
Pages:
- Type in "faq/*"
Save the block. Now your question list with links to answers further down the page should be appearing at the top of your FAQ pages!
7. Next, you need a list of your categories. Sadly, there's no easy way for Drupal to do this. Views has a view type for taxonomies which is unfortunately not very powerful and so we can't use it to get links to "/faq/[term]" as it doesn't allow us to rewrite our own links. It allows you to link to term pages but those pages are rendered by either the Taxonomy module or that default view I mentioned earlier. Trying to override those paths with our own view is a mess, probably due to weighting.
Anyways, we can easily create a list of topics with links with a tiny bit of PHP. Create a new block using the Block admin page. In the block body:
<?php $tree = taxonomy_get_tree($vocabulary_id=YOUR_FAQ_VOCABULARY_ID_HERE); $html = '<ul>'; if ($tree) { foreach ($tree as $term) $html .= '<li>' . l($term->name, 'faq/' . str_replace(' ', '-', mb_strtolower($term->name))) . '</li>'; } $html .= '</ul>'; return $html; ?>
Again, set this block to only appear on "faq/*" pages. Then configure its location into a sidebar or somewhere and you will be displaying links to each of your FAQ topics and you're done!
Mass editing nodes in Drupal can be a chore. If you just want to edit some attribute and set it to a single value for many different nodes then Views Bulk Operations can help. But you may instead want to make some quick changes to the titles, bodies, and CCK fields of a bunch of nodes of a certain type. The default way would be to open each node in a browser tab, go to their edit forms, and then operate on each one individually. Can we come up with a better way?
Multi Node Edit is a module that sounds like it could help. It promises to give you a page with many node edit forms. Unfortunately, this is not the case. By itself, this module is not usable to users. If you write some code to use it like a library you might be able to create a page with many node edit forms on it.
Recommendation: This module isn't useful to most people. It might be useful if you're writing code for a custom page.
Editview (editview) is the easiest to use, you just build Views and select 'Style' of 'Editview' in one of your displays. It works on the whole display, so all fields become editable. Unfortunately, you can't control this. Some fields shouldn't be editable, and so sometimes they just don't appear at all. Other times you may want to view a field but not edit it. Unfortunately, this isn't possible, even when using Global fields. [This issue is tracked here: http://drupal.org/node/635076]
* In my case, I just wanted to get a link to edit the node. I accomplished this by adding a Node: Path field which shows the path/alias editing form.
Recommendation: Use Editview if you're comfortable with building Views. Then you can construct a view of the fields you want to edit, and make it easy to filter the nodes you want to edit.
Editable Fields (editablefields) - 6.2 doesn't integrate with Views, despite the README. 6.3 does integrate but not the way explained in the README.txt. There is no added View type to choose when creating views. The exported content types used for demonstration don't import either. Most fields aren't editable, but if they are there will be a new checkbox for editable with some options. However, the 6.3 version of the module is still quite buggy so it doesn't work at all.
Recommendation: Avoid Editablefields module for now unless you want to just edit fields in the node display.
- tomo's blog
- Login to post comments
- Comments
Drupal sites, like any content sites, naturally receive a lot of spam comments. Any blog or blog-like site which allows unauthenticated or anonymous users to comment on posts or any content is open to spam because spammers just want to have links to their websites displayed by your website for link-building purposes which is important for SEO. Spam comments used to be much more obvious as in blatant ads for penis-enhancing drugs. Nowadays, spammers are using more sophisticated scripts that are still easy to detect - by humans and usually also by spam-detection software. New spam might praise your blog or quote something you said in your blog post or just say something generic, perhaps on a related topic or perhaps not related at all. Then they will have their spam website in the URL field, if not also in the comment text. If the website has keywords which are totally unrelated to your blog, you can bet it's spam.
What can a Drupal website do about spam?
One old solution for spam, a common plugin on WordPress sites, is called Akismet. Akismet is a service and requires you to create an account and get an API key from them. Then they will help you detect spam.
Drupal has something like Akismet but improved and it was created by the Drupal creator himself. It's called Mollom, and it's also a cloud-based service, and you also create an account with them. Mollom is free to use although there are some paid services. I use Mollom on this blog. It catches hundreds of spam comments for me but also lets in a single false negative per day or so. I wouldn't have been able to turn on comments without turning on Mollom as I would immediately be deluged with spam. But I hope that Mollom continues to improve so that I don't even get 1 spam on most days.
Since I still do get some ham and spam comments, I have to process them somehow and report the spams to Mollom so they can improve their algorithms and heuristics for the future. Unfortunately, Drupal's default comments management view doesn't easily let me see if a comment is spam or not. In order to make that decision I need to see the URL and comment text that was saved. If there was no URL, it usually isn't spam, and if there was no URL in the comment text then most certainly it isn't spam. But if there was a link in the text, I need to see what site it links to, and see if the comment is at all intelligent or relevant. You can also tell by looking at any link saved in the URL field. Since Drupal's comment management page doesn't do this, I created a view, which you can import (this is Drupal 6) below.
The thing that Drupal's comment admin page offers is bulk operations. Mollom provides some operations such as report spam to Mollom and delete. This is usually what I want to do. Unfortunately, there's no way to use Mollom's actions in Views with Views Bulk Operations! So until this issue gets resolved (http://drupal.org/node/655846) I've done the next best thing which is to have links to the Mollom management page for each comment. It will still require you to open that page, select the reporting action you want, and then submit the form though. But this workflow has at least saved me some time and now I have removed all the really spammy spam from this site.
$view = new view; $view->name = 'comments_more'; $view->description = ''; $view->tag = ''; $view->view_php = ''; $view->base_table = 'comments'; $view->is_cacheable = FALSE; $view->api_version = 2; $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ $handler = $view->new_display('default', 'Defaults', 'default'); $handler->override_option('fields', array( 'name' => array( 'label' => 'Author', 'alter' => array( 'alter_text' => 0, 'text' => '', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'link_to_user' => 1, 'exclude' => 0, 'id' => 'name', 'table' => 'comments', 'field' => 'name', 'relationship' => 'none', ), 'homepage' => array( 'label' => 'Author\'s website', 'alter' => array( 'alter_text' => 0, 'text' => '', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'display_as_link' => 1, 'exclude' => 0, 'id' => 'homepage', 'table' => 'comments', 'field' => 'homepage', 'relationship' => 'none', ), 'comment' => array( 'label' => 'Body', 'alter' => array( 'alter_text' => 0, 'text' => '', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'exclude' => 0, 'id' => 'comment', 'table' => 'comments', 'field' => 'comment', 'relationship' => 'none', ), 'delete_comment' => array( 'label' => 'Delete link', 'alter' => array( 'alter_text' => 0, 'text' => '', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'text' => '', 'exclude' => 0, 'id' => 'delete_comment', 'table' => 'comments', 'field' => 'delete_comment', 'relationship' => 'none', ), 'edit_comment' => array( 'label' => 'Edit link', 'alter' => array( 'alter_text' => 0, 'text' => '', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'text' => '', 'exclude' => 0, 'id' => 'edit_comment', 'table' => 'comments', 'field' => 'edit_comment', 'relationship' => 'none', ), 'hostname' => array( 'label' => 'Hostname', 'alter' => array( 'alter_text' => 0, 'text' => '', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'exclude' => 0, 'id' => 'hostname', 'table' => 'comments', 'field' => 'hostname', 'relationship' => 'none', ), 'status' => array( 'label' => 'In moderation', 'alter' => array( 'alter_text' => 0, 'text' => '', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'type' => 'yes-no', 'not' => 0, 'exclude' => 0, 'id' => 'status', 'table' => 'comments', 'field' => 'status', 'relationship' => 'none', ), 'timestamp' => array( 'label' => 'Post date', 'alter' => array( 'alter_text' => 0, 'text' => '', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'date_format' => 'small', 'custom_date_format' => '', 'exclude' => 0, 'id' => 'timestamp', 'table' => 'comments', 'field' => 'timestamp', 'relationship' => 'none', ), 'view_comment' => array( 'label' => 'View link', 'alter' => array( 'alter_text' => 0, 'text' => '', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'text' => '', 'exclude' => 0, 'id' => 'view_comment', 'table' => 'comments', 'field' => 'view_comment', 'relationship' => 'none', ), 'cid' => array( 'label' => 'Mollom', 'alter' => array( 'alter_text' => 1, 'text' => '<a href="/mollom/report/comment/[cid]">Report to Mollom</a>', 'make_link' => 0, 'path' => '', 'link_class' => '', 'alt' => '', 'prefix' => '', 'suffix' => '', 'target' => '', 'help' => '', 'trim' => 0, 'max_length' => '', 'word_boundary' => 1, 'ellipsis' => 1, 'html' => 0, 'strip_tags' => 0, ), 'empty' => '', 'hide_empty' => 0, 'empty_zero' => 0, 'link_to_comment' => 0, 'exclude' => 0, 'id' => 'cid', 'table' => 'comments', 'field' => 'cid', 'override' => array( 'button' => 'Override', ), 'relationship' => 'none', ), )); $handler->override_option('access', array( 'type' => 'none', )); $handler->override_option('cache', array( 'type' => 'none', )); $handler->override_option('css_class', 'view-comments-more'); $handler->override_option('header', '<style> .view-comments-more table { background: white; position: relative; z-index: 100; } </style>'); $handler->override_option('header_format', '2'); $handler->override_option('header_empty', 0); $handler->override_option('items_per_page', 30); $handler->override_option('use_pager', '1'); $handler->override_option('style_plugin', 'table'); $handler->override_option('style_options', array( 'grouping' => '', 'override' => 1, 'sticky' => 0, 'order' => 'desc', 'columns' => array( 'name' => 'name', 'homepage' => 'homepage', 'comment' => 'comment', 'delete_comment' => 'delete_comment', 'edit_comment' => 'edit_comment', 'hostname' => 'hostname', 'status' => 'status', 'timestamp' => 'timestamp', 'view_comment' => 'view_comment', ), 'info' => array( 'name' => array( 'sortable' => 1, 'separator' => '', ), 'homepage' => array( 'sortable' => 1, 'separator' => '', ), 'comment' => array( 'separator' => '', ), 'delete_comment' => array( 'separator' => '', ), 'edit_comment' => array( 'separator' => '', ), 'hostname' => array( 'sortable' => 0, 'separator' => '', ), 'status' => array( 'sortable' => 0, 'separator' => '', ), 'timestamp' => array( 'sortable' => 1, 'separator' => '', ), 'view_comment' => array( 'separator' => '', ), ), 'default' => 'timestamp', )); $handler = $view->new_display('page', 'Page', 'page_1'); $handler->override_option('path', 'views/comments_more'); $handler->override_option('menu', array( 'type' => 'none', 'title' => '', 'description' => '', 'weight' => 0, 'name' => 'navigation', )); $handler->override_option('tab_options', array( 'type' => 'none', 'title' => '', 'description' => '', 'weight' => 0, 'name' => 'navigation', ));
- tomo's blog
- Login to post comments
- Comments
Drupal's Views module has a lot of hooks that can be used to modify the behavior of a hook, from building the query to putting together the output. Hooks are also used by modules which want to add to the Views building interface, even the basic node-based Views.
Views supports a number of hooks which aren't documented. One I came across recently was hook_views_query_substitutions. This is potentially a powerful hook, one which the module Views arguments in filters (currently a sandbox module only for developers) takes advantage of to allow filter value substitution from the Views UI.
Hook hook_views_query_substitutions is pretty basic. By implementing the hook, you return an array of values you want to substitute with the keys you want to substitute for. This is how the magic values like ***ADMINISTER_NODES*** which you see in the Views query preview get turned into valid SQL.
For example:
function user_views_query_substitutions($view) { global $user; return array('***CURRENT_USER***' => intval($user->uid)); }
After this hook is called, Views will then run str_replace with the keys of the substitutions array and its values and apply it to the breadcrumb, title, and views arguments, as well as the query string that has been built so far.
Hook hook_views_query_substitutions gets called in the execute() call of Views. The first thing execute() does is build(), which you can hook into with hook_views_pre_build, hook_views_query_alter, and views_post_build (in that order), then you can hook into hook_views_pre_execute and hook_views_post_execute (the latter happens after the views query is executed and shows an unsubstituted query, there is no hook where one can see the substituted query). You also have a chance to hook into the db_rewrite_sql call.
Module "Views arguments in filters" is pretty basic and only allows substitution in filters which allow you to enter open text (so that you can set a value like '%1'). But by doing something similar, we can use a string passed in as a Null argument to command a Drupal module to do even more, like string substitution on any part of the query. I have a module in development now that does exactly that.
- tomo's blog
- Login to post comments
- Comments
[This blog post is a rewrite of just the main points due to my baby Macbook Pro dying while I was distracted.]
Drupal content types with CCK make it quite easy to add any number of defined fields to an 'object', and with multiple/unlimited values for a field or with node references it's possible to make a Drupal node 'two-dimensional'.
Sometimes you need more. Sometimes you want tabular data, a table, to be part of a node. If the table always has the same dimensions, and at least the same columns for each node, then the above can work through node references and views.
What if you want to add a different two-dimensional table to nodes of a content type, but without knowing the number or labels for the columns and rows beforehand. For example, you might want to attach a pricing table to a node, with multiple products and multiple ways to price each product. An example of that might be 5 t-shirt designs, where shirts are priced based on size and quantity ordered.
Read the rest of this article...Late night hack:
You have a lot of fields in a lot of content types. You're creating a view with new display fields but it's a pain to find just the content type fields you want. Wouldn't it be cool if you could just select a content type from a pulldown and see content fields filtered to just the ones in that content type?
Add this bookmarklet to your bookmarks bar:
Views UI Filter
Recent comments
1 year 11 weeks ago
2 years 3 days ago
2 years 1 week ago
2 years 3 weeks ago
2 years 19 weeks ago
2 years 19 weeks ago
2 years 19 weeks ago
2 years 19 weeks ago
2 years 19 weeks ago
2 years 19 weeks ago