Development

Symfony12AdminGenerator

You must first sign up to be able to contribute.

Version 44 (modified by FabianLange, 9 years ago)
updated admin gen ticket status list

Request For Comments: Symfony 1.2 Admin Generator Development

This page serves as place where features and modifications of the admin generator in symfony 1.2 can be discussed.

If you want to comment a paragraph, just add a bullet below it with your name, like this:

 * comment from __John Doe__
   * Maybe this is dumb

Which will be rendered as:

  • comment from John Doe
    • Maybe this is dumb

The main goals of the development of the generator are:

  1. Provide a stable basis for upcoming extensions
  2. Allow the reuse of admin components in different admin views
  3. Make the generator more extendable by the developer and by plugins

The goals of the development are NOT:

  • Develop a full-stack admin generator
  • Provide all the features of the old admin generator (this will be done in 1.3)
  • Provide any sorts of Javascript functionality (will be provided through plugins)

(full credits for the style this introduction go to Nicolas' RFC page :-)

Use Cases

Let's collect some use cases to describe how the configuration for different requirements could look like.

Use Case: Default View

I want to display and modify a list of articles. I want to be able to browse the articles and to modify them.

class ArticleAdmin extends sfAdmin
{
  // using a similar syntax as in sfForm could ease the use
  public function configure()
  {
    // No configuration is required, since all the default settings are used.
    // The class definition is required to tell the generator that this admin view exists and that it is displayed in the menu (or whatever)
  }
}

Use Case: Customized List View

I want to display a list of articles. The list view should display only the title of the article linking to the edit action and the time of its last modification.

Display the title "List of all articles". Each page of the paginated result should contain 5 articles. Sort by the field 'title' ascendingly.

A virtual field 'author name' should be included, which is composed of the concatenation of 'author_firstname' and 'author_surname'.

class ArticleAdmin extends sfAdmin
{
  public function configure() {}
}

// This class is automatically used if it exists
class ArticleAdminGrid extends sfAdminGridTabular
{
  public function configure()
  {
    $this->setWidgets(array(
      'title' => new sfWidgetAdminText(array('link' => true)),
      'updated_at' => new sfWidgetAdminDate,
    ));
    // ...
  }
}

Use Case: Filter the List View

I want to filter the list view by creation date. Additionally, I want to be able to do a fulltext search in the fields 'title' and 'author_name' (We could incorporate the separation between filter (preconfigured values) and search from Django's administration).

Use Case: List Field Credentials

The user should only see the field 'is_important' when he has the credential 'editor' or 'publisher'. Only users with the credential 'editor' or 'publisher' can see the field 'is_published', but those with the credential 'publisher' are able to click on the field in the list view to toggle the value.

Use Case: List Actions

The user should see the button 'edit' and 'delete' next to each article in the list.

* Comment from Gary Feldman (GaryFx):

  • Only users with the editor or publisher credential, or a user who is the author of a specific article will be able to edit it. Thus an editor will see an edit button for all articles, but an author without editor credentials will only see an edit link next to those articles he wrote.
  • The visual designer will be able to choose between showing disabled edit links as dimmed or not showing it at all.

Use Case: Customized Edit View

Display the fields 'title', 'text' and 'is_published' as regular widgets. 'created_at' should also be displayed, but only as text.

'created_at' and 'is_published' should be displayed in the fieldset 'Header', while 'title' and 'text' are displayed in the fieldset 'Body'. The field 'text' should contain the help text 'Please insert a text with max. 1024 characters'.

After saving, the browser should be redirected to the list view.

* Comment from Gary Feldman (GaryFx):

  • The user can choose between saving and going to the next article, saving and going to previous article, saving and going back to list.
  • When going back to the list, it goes back to the same page, if the list spanned multiple pages, using the same filter and sort.

Use Case: Edit Field Credentials

The user is only able to modify the 'title' and 'text' fields when he has the credential 'publisher' or 'editor'. Otherwise he sees the plain text. When the user is 'publisher', he is able to see and modify the field 'is_published'. When he is 'editor', he is just able to see it. In all other cases 'is_published' is hidden.

Use Case: Edit Related To-One Records

In the edit view for my article, I want to modify also the related author which has the fields 'firstname', 'surname' and 'description'. One article always has an author. It should be possible to embed the same author form in a different admin view for the news items.

If I create a new article, I want to be able to associate an existing author with this article.

Use Case: Edit Related To-One/Zero Records

The article should be assigned to a given category. There are articles though which do not belong to any category. I want to be able to assign existing categories to the article or create a new category. It is not necessary to display the whole form of the related category in the article form.

Once again, this functionality should be reusable for the edit view of the news items.

* Comment from Gary Feldman (GaryFx):

  • The visual designer can choose between using radio buttons or a drop down list for the category.
  • Related to many: The user can choose zero or more categories, perhaps with checkboxes, perhaps with a multiselect list.

See also: * Admin Generator 1.2 Sprint Notes


Concept by Bernhard Schussek

Please note that this section does not contain official information. The text written in here is solely based on my personal concepts.

I was planning to rewrite parts of the admin generator together with a few members of the symfony usergroup vienna starting in summer '08. I developed a concept of different UI and technical enhancement that are described here. The purpose is to discuss whether some or all of these features can be integrated into the new admin generator. I am willing to spend time and probably money to develop these features, as may be some other people I know personally.

Motivation

The motivation of this rewrite was mainly to increase the usability of the admin generator, so people without much technical knowledge can easily use the interface without lots of configuration/adaption.

In the concept I tried to find generalized data representation schemes which can be used in most use cases, but still are usable enough to be used by inexperienced users.

Notes

  • Most of the ideas presented in here are based on successful applications such as Silverstripe, Jacomo (a very usable and customizable db administration tool developed by my previous employer) and Joyent (a collaboration tool with a very innovative UI).
  • I'm basing my concepts on the current syntax of configuration files. These need to be modified in case the new admin generator makes radical changes here.
  • Many of the examples are incomplete and require further discussion
  • I'm refering to inexperienced, non-technical users when talking about "users"

Besides: I was planning to develop this generator based on Doctrine because of the easier syntax and better support of relations, inheritance and nested sets.

Major Additions

Modification of Contextual Records

Problem

In many applications the business model can be (mostly) separated in two groups:

  1. Informational Entities (valuable information), for instance
    • Article
    • Town
    • Customer
    • Booking
    • Page
  2. Contextual Entities (providing contextual information for the above, mostly "groups" or "categories"), for instance
    • Group
    • Tag
    • Category
    • Type

(of course this separation does not apply to all models, but from my experience to many of them)

The difference between them is that users generally don't want to know about the second.

Example:

For instance, the model may require to define a related "AccommodationType" for "Accommodation" records. Real users will not be interested in the type. They want to create a new accommodation, defining (and eventually creating) a type is only a burden, especially when the type has to be created in a different list view. Users will enter the accommodation creation form, fill half of the fields only to realize that the related type does not exist yet. Advanced users will open the type list in a new tab or window, but inexperienced users will just leave the form and lose all their entered data.

The only real use for the user in such contextual entities is the ability to filter list views (in this example - filter accommodations by type).

So our tasks are:

  1. Make modification of contextual entities a contextual task that doesn't distract the user from doing his actual task
  2. Make filtering by those contextual records easy

Concept

I came up with the following concept when I stumbled upon Joyent Connector while analysing different web applications:

  • Add contextual records through sub-forms while modifying the informational entities (solves 1.)

Example:

Create AccommodationTypes while modifying an accommodation through subforms

Concept Screenshot

  • Display, add and modify contextual entities in a tabbed sidebar in the views of the informational entities they're related to. Don't waste space by creating a dedicated list view which users will never use (solves 1.)

Example:

Display AccommodationTypes in Accommodation list and edit views, create, delete and modify them using AJAX

Concept Screenshot (forgot the delete button there, but I think you know what I'm up to)

  • Filter a list of informational entities when clicking on a contextual entity in the tabbed sidebar. Users know this behaviour from browsing f.i. file browsers, music applications etc. (solves 2.)

Example:

When the user clicks on an AccommodationType entry in the sidebar of the Accommodation list view, filter the list by this type.

Concept Screenshot

Realisation

One could realise the sidebar with the following configuration:

generator:
  param:
    model_class: Accommodation
    sidebar:
      # Configuration of the sidebar tabs
      display:
        "Types":         [_types]                                                # custom field that can be overridden by a partial
        "Tags":          [_tags]
      # Configuration of the sidebar widgets
      fields:
        types:           { type: sfSidebarTree, param: relation=Type }           # uses sfSidebarTreeAdminWidget
        tags:            { type: sfSidebarTree, param: relation=Tags add=false }

(Note: this code sample is probably incomplete)

This way, you can create your own widgets for the sidebar and the syntax remains very similar to the configuration of the edit view (see below).

Adding a contextual entity through a subform while editing an informational entity can be handled through a widget (see below).

Widgets

The generator should support widgets, meaning a generalized way of representing and modifying record data. These widgets as I speak of them are different from symfony 1.1 form widgets in that they incorporate view, model (validation) and partially business logic.

Use case 1:

The record class "Accommodation" contains a many-to-one foreign relation "AccommodationType". When editing an accommodation, the user can select its type. Additionally he should be able to create a new type if the desired type does not exist yet. This can be done through a subform for adding the record.

generator:
  param:
    edit:
      AccommodationType: { name: Type, type: SelectRelation, param: add=true } # Uses SelectRelationAdminWidget, the 
                                                                              # original record and the name of the 
                                                                              # field (relation in this case) are 
                                                                              # automatically provided to the widget

Concept Screenshot

The (conceptual) widget incorporates the following logic:

  • display of all available types (model, view)
  • addition of a new type (model, business logic)
  • validation of selected types (model)

Use case 2:

The record class "Accommodation" contains two fields "longitude" and "latitude". They should be modified through a Google Map with a draggable marker. These fields do also appear in other classes, so a reusable widget is needed which can be configured in all classes:

generator:
  param:
    edit:
      _location: { type: GoogleMap, param: longitude=longitude_field latitude=latitude_field } # Uses GoogleMapAdminWidget 
                                                                                               # with the given parameters

Concept Screenshot

The widget incorporates the following logic:

  • display of the map, javascript (view)
  • validation of the coordinates (model)

Simultaneous Modification

This is not as important, but useful anyway.

The user should be able to modify several objects at once by selecting them (see Enhanced List View Actions below) and pressing the button "Edit". When multiple records are modified, the form contains a checkbox "Modify" for each form field which is unchecked by default. Form fields which are the same in all selected records are filled out, the other ones are left empty. If the user changes the content of a form field, checks "Modify" and saves the record, the given property is changed to the new value on all objects.

This feature is very useful for batch editing of all sorts.

Concept Screenshot

Support of Nested Sets

The generator should be able to automatically display nested sets as trees in the list view. Nested sets do always represent hierarchical data and thus can basically always be represented by trees, which are more intuitive to use for people than plain lists.

There shouldn't be any further configuration necessary to enable this feature.

Concept Screenshot

Support of Inheritance

The generator should be able to automatically deal with Doctrine's inheritance schemes. I have too little experience in terms of real use cases to come up with a usable concept here though.

Support of I18N

The generator should be able to deal with translated records. How to do this in detail needs to be discussed.

Minor Additions

The admin generator should feature the generation of a global navigation module linking to the index/list actions of different modules. This could easily be incorporated in a YAML file /apps/myapp/config/generator.yml:

generator:
  param:
    display:
      myModule:  ~                                                        # automatically links as "My module" to myModule/index
      myModule2: { name: "Site", icon: "...png", action: myModule2/edit } # further configuration

Alternatively, this information could be stored directly in the generator.yml (with the disadvantage that custom modules cannot be linked anymore without creating a generator.yml for it)

generator:
  param:
    navigation:    { name: Site, icon: ...png, default_action: edit }

Tabs

It should be possible to group fields into tabs in the edit view. This can be done by reusing the current configuration syntax for fieldsets:

generator:
  param:
    edit:
      display:
        "Details":  [name, email, _location]
        "Rooms:     [...]
        "Prices":   [...]

Concept Screenshot

Enhanced List View Actions

The support of custom actions in the list view should be enhanced. There should be two types of display modes:

  • normal (normal button that does something)
  • collapsible (the visibility hidden partial is toggled when the button is pressed, that could contain filters for the list view etc.

Additionally one should differ between to types of action scope:

  • none (the action does not affect a specific record) - for instance "Create", "Filter"
  • selected (the action affects only selected records) - for instance "Delete", "Edit". One or more records need to be selected using checkboxes in the list view first
generator:
  param:
    list:
      actions:
        _create:  { name: "New..." }                  # Default values: { display: normal, scope: none }
        _edit:    { scope: selected, action: edit }   # The selected record identifiers will be handled to the specified action
        _delete:  { scope: selected, action: delete } 
        _filter:  { display: collapsible }            # The partial _filter.php is toggled open/closed when the button is 
                                                      # pressed and contains a form used to apply specific filters

Concept Screenshot

Francois: "Selected" scope actions already exist in the symfony 1.1 admin generator, but with a different syntax (batch_actions). I'm not sure about the opportunity to break BC here.

Enhanced Support of Relations

The admin generator should provide displaying, sorting and filtering by columns of related records in an intuitive and easy manner:

generator:
  param:
    list:
      display:  [name, AccommodationType.name]         # The field name of the related AccommodationType record is displayed and
                                                       # can be used for sorting
      filters:  [name, AccommodationType.name]         # One can filter by the attribute "name" of the related AccommodationType
      fields:
        AccommodationType.name: { name: Type }         # Configuration of the displayed field name

Leon (LvanderRee?): Hi Bernhard: nice to see you like this idea as well, there is one problem though with your current syntax and that is thay you cannot use the ObjectName to define the related Table, you should use the ForeignKey in order to be able to support multiple links to the same RelatedTable (For example created_by/username, updated_by/username (I use a slash / instead of a dot . since I am used to this, but the dot is fine by me as well.)

Bernhard (bschussek): Hi, thanks for your input! Actually, "AccommodationType" is a Doctrine relation. I just gave it the same name as the class it points to for the sake of clarity. You could replace it with "Type", "Types", "LastCreator", "LastUpdater" or whatever the relation is named. The dot is borrowed from Doctrine's DQL, where exactly the same syntax is used an which I am used to. For propel, the syntax could be different.

Some quick ideas from Matthias Nothhaft (mahono)

  • Please have a look into the store backend of Magento (which may be a generic inspiration as well)
    • I would like to have filters in the head of the list table (each field in its matching column) as implemented in the Magento backend.
  • I would like to have personalized configuration.
    • Each user should be able to enable/disable columns/fields and reorder them as he likes it.
  • Primary key(s) must be editable

Conclusion: Most important is a much more modular system so people can plug in their own alternative implementations of the various parts of the admin magic without having to implement a complete new admin generator just because you want to change a feature. Factories/drivers/Widgets or whatever to provide different implementations of all parts of the admin generator?

Some notes from Tom Boutell at P'unk Ave (boutell)

The ability to manually order items is desirable. But more than that, the ability to create an extension that adds such a capability without bad coding practices is desirable. I'm going to pick on sfPropelManualOrderPlugin here, not to be mean but to illustrate the underlying difficulties its developer faced.

The existing sfPropelManualOrderPlugin looks great and works well... but I never looked at it closely under the hood until today. It turns out that in order to add the sorting feature, sfPropelManualOrderPlugin must contain not only a complete template theme (which is also unfortunate, but is at least apparent on first inspection) but also a complete copy of data/generator/sfPropelAdmin/sortable/template/actions/actions.class.php, the admin class generator itself.

This means that if the admin generator gets security fixes, bug fixes, etc., they are not available to folks relying on sfPropelManualOrderPlugin, who may not even be aware that this is the case. Ouch.

And in any case it's not possible to have two or more such "extensions" to the admin generator in play at the same time.

Why did the author of sfPropelManualOrderPlugin do this way? Because there was no apparent alternative way to deliver the feature.

You can add features to an admin generator class easily on a one-module-at-a-time, application-specific basis, by extending the autogenerated actions class and then overriding single methods like addFiltersCriteria, then returning the result of a call to parent::addFiltersCriteria, etc. But there is currently no good way to share these improvements in a reusable fashion.

Mix-ins can be used to address this problem, but it's quite painful- the application developer hoping to use a plugin that extends the admin generator will need to follow the techniques here:

http://www.symfony-project.org/cookbook/1_0/en/behaviors

Overriding every method that might get further overridden by any of the admin generator extension plugins they hope to use in order to ensure the mixin methods get called.

Even then, the problem of template files (themes) for multiple such extensions simultaneously in use hasn't been resolved. Of course if two plugins alter the same template that's a genuine conflict, but if they alter different templates, there ought to be a simpler means of electing both at once.

Some notes from Gary Feldman (GaryFx)

I think I got hung up on the term contextual when I first read this, because contexts are generally transient while the things here are clearly persistent. At first I thought the difference was between concrete and abstract (which matches the example but isn't really the idea). The more I think about it, the more I think it's the difference between primary and secondary data. For example, you might be designing a set of rooms, adding furniture to each room. The user has a given context, but the items are all concrete. But in a different context, the same user might be configuring a specific bed, specifying the mattress type, number and type of pillows, linens, etc. So a context represents a particular user task, and each context would have some primary data item and some related, subsidiary (or secondary) data items. Some data tables might always be primary, others might always be secondary, but there could still be some that play different roles at different times. There's nothing in your design that prevents that, but I think the terminology of primary versus secondary (or something similar) captures the idea more clearly, at least for me.

I see a need for fine-grained access control. Using your AccommodationType? widget example, it's possible that many people could be authorized to assign a type to an accommodation, but only some of them might have permission to create new ones. There needs to be a way to express and enforce this.

I also don't see this as being specific admin-related, but then the same is true for the original Admin generator. Really what this is about is using YAML to describe complex form layouts.

Bernhard (bschussek): Thanks for your interesting entry! I didn't think about this subject the way you did, but actually I agree. Contextual entities, as I called them, may not always and in every view be contextual. I don't know whether renaming them to primary and secondary data items solves this issue, as again the "primary" is not explicitely restricted to the current view on the model. Focusing a bit more on the term "view" (as in UML) may be the solution, what do you think?

I agree that it should be possible to have fine-grained control over the permissions of different parts of a widget. As Ivan mentioned, YAML might not be the ideal solution anymore for configuring such a lot of details. Fabien already mentioned in a mail that he wants to move the configuration of the generator from YAML to PHP classes (probably like the new Form framework), so discussion about YAML details might be obsolete anyway.

Gary Feldman (GaryFx): I think that focusing on the term view feels right, but I'm not sure where to go with it. Perhaps current view and related view? I'm not really sure, but it's the sort of question I'd propose to less experienced users, since they're the ones who are most in need of meaningful terms.

As for YAML, I'm really rather fond of it; I think it's one of the best parts of symfony. I'm not sure what the objections are, but that's really another discussion.

Some notes from Ivan Zgoniaiko (lking)

Sorry for my bad english (if you found any error, please edit it or delete note if it looks stupid).

First of all thank you for so great concept. Looks like admin generator will be very powerful, like 2 clicks and you have full-featured admin for your module.

Current admin generator works ok when you need to build it for 1 table with few fields. But we have prob with sort and filter by FKs (sorting by FK just don't work and filter by FK is select). Here no any probs with filter with select if you have few rows in it, but here can be a lot of rows. At this way page will not be loaded. Sure we can solve it, we copy/create new template for this filter and make autocoplete for example. But this is good for basic logic. But here can be more complex logic. For example if we have few admins with diff permissions:

1-st admin can see/edit/create everithing and 2-nd admin have access to create/edit only 1 type of objects, and see only 2 types of objects but can't see AccommodationType?. We have admin config for objects with FK, select of FK object is required. 2-nd admin can create/edit this objects, but can't even see FK objects (AccommodationType?). As result 2-nd admin can't save new/edited object because he can't fill required FK.

Additionally here can be different rules for FK objects. Like we want to create new object, but FK required. Here no needed FK object in list. Create it, but this FK object require FK objects of 3-rd and 4-th types. No this objects too, create them. And this chain can be very-very long. As result we'll recive 1 large config for all application in 1 YAML file.

Another prob that here no inheritance of configs for admin generator (at least i didn't found how to do it). Maybe here no sense to use YAML for admin generator at all? I don't want to say that YAML is evil, it is very powerful. But maybe it not so good for admin generator? We used YAML for form validation with symfony 1.0, and with symfony 1.1 we use php. Maybe here is sense to use skeleton for admin like we use skeleton for CRUD?

Some notes from Klemens Ullmann (klemens_u)

I'd like to configure access right per field. But not only on/off as it is now, but read/write access. This would be handy to create "profiles" for different user-groups. Example Users: Admin can edit all fields, Users can just edit some fields like the phone-number. All other fields are not editable, but shown to the user

This implies, that every widget has a "show" and a "edit" method. Per default the "show" method would just print out the value. For foreign keys it would print out the output of the _ _toString() method. Another example: a widget "email". The "edit" method would return a <input type='text /> tag (Using the forms framework's sfWidgetFormInput widget :-). The "show" method would automatically create a clickable link for the email-adress. And the widget's validator would automatically validate if it's a valid email-address.

(my email: klemens[DOT]ullmann[AT]ull[DOT]at)

Some notes from Romain Stévant (Jackovson)

I agree with Gary Feldman (GaryFx) : I think there is no need of YAML for the admin generator. All tweak possibilities in YAML can be done with PHP and inheritence, and MANY more can be done this way. At my office, we work with a another framework that have a very powerfull admin generator, based on generic PHP files (view, edit, list controllers and templates) that can be inherited to make all tweaks possible : controller side and template side. Some tweaks are juste properties (all YAML could be done like that), others ones are modthed redefinition (mfor ore complex tweaks).

Gary Feldman (GaryFx): Actually, it was Bernhard's response to my submission that suggested avoiding YAML. As I just replied, I like the YAML. Sure, an imperative language like PHP can express anything that a declarative language can (and vice-versa, for a suitably powerful declarative language). But a large part of configuration is just naturally declarative.

An goold example of this powerfull system : you wan to display comments of an article when showing/editing an article. With our framework, we just have to add one line in the controller (to select related comments for an article), and a line in the template (to display related elements). This way, you can display very easily related data for an element. The display of relateds elements is handled by admin generator for theese elements, so templating tweaks are refactored.

Templates are juste classes (with name convention) with a main method (for example display()) that will display HTML code (not direct included PHP files... I think this should be done into Symfony too, to have a more powerfull and refactored templating system, but thats out of the present topic !). Commonly, a template class will have many methods, private and public, and public ones can be considered as partials. The generic generator have many methods used to display all elements on the page : fields title, fromating fields value (example : display icons for boolean or enum fields) etc. Thats could look like :

 getFieldTitle($fieldName){}//get title for fields
 formatFieldValue($fieldName, $value){}//format a field value
 etc

You just have to know public generic methods to do your tweaking job. (I hope I have been clear... but I am not sure :p)

Antoher idea concern "filters". In our frameworks, I think filters are much more powerfull than in Symfony (1.0/1.1) : you have the possibility to graphically build an SQL query with as many "WHERE" condition you want (WHERE AND AND AND.... no OR for the moment, but it could also be great). So you have a "add a condition" link, that display a dop down list of object proporties (id, name, author, etc). When selecting a property, antoher drop down list appear with all condition type (equal, greater than, containing...), and a text field to add your value (for int or string fields). For "special" fields", the second drop down + text field is replaced by special input : for exemple a yes / no radio button for boolean, unique drop down list with related elements (like categories for a article), etc... This way you have much more powerfull filters.

Another (gadget) feature is the possibilty to choose the displayed rows (in displayble fields) : very usefull for elements that have many fields.

I can post screenshot if I am not clear ;)

(I am sorry for my bad english...)

I hope this will help.

Ticket Tracking

There are some tickets scheduled for 1.2 which do contain wishes for Admin Gen. This makes it possible to tick them off with (fixed, wontfix, invalid) after the new admin gen is in place. I haven't checked them to see if they make sense

  • #3037 double list self referencing issue

plus tons of others that are not yet scheduled for 1.2 but for earlier releases as bugfixes. Anyone volunteering to scan these? :-)

#1806 , #3299 , #3949 , #1658 , #2489 , #2812 , #2813 , #2908 , #2932 , #3190 , #2474 , #1451 , #1805 , #1849 , #1992 , #2127 , #3063 , #3437 , #3617 , #3826 , #3925 , #3944 , #4077 , #4120 , #4178 , #2806 , #4360 , #924 , #1052 , #2013 , #2092 , #4012 , #4477

Fixed in 1.2:

#685 , #2929 , #446 , #2953 , #4298 , #2203 , #2390 , #4207 , #4312 , #4190 , #4022 , #4339 , #3870 , #2238 , #2952 , #1620 , #2423 , #2574 , #2975 , #1592 , #4098 , #1507 , #2496 , #2907 , #2909 , #1524 , #1020 , #2010 , #2909 , #1627 , #2954 , #1657 , #2924 , #3051 , #3198 , #3360 , #345 , #1437 , #1622 , #1754 , #2122 , #2604 , #4471 , #2974 , #4542 , #1521

Attachments