Please upgrade here. These earlier versions are no longer being updated and have security issues.
HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.

Kindly asking for help with a plugin

I've created a plugin that is already working, but quite ugly and not finished. The plugin should do following:
1. if user1 opens profile of user2 a db entry is created
2. if user2 looks at his own profile he has a new menu entry
3. that menu entry shows links to a view that lists every user who clicked his profile

Look at the screenshots and you will see and understand. The plugin is for Vanilla 2.1b2 and can be downloaded here: https://github.com/R-J/ProfileVisitors/archive/master.zip

I already have it working but there are some things that are not working by now and here are my questions:
1. my view doesn't look like any other profilecontroller view. Could anybody tell me what I have to add or have to change?
2. my list should look like the activity list in profile but it does not. When there are no profile pictures, there is no space left free
3. in activity view there is some extra text that makes images fit into rows height. My rows are too small. I want rows with pictures be big enough and rows without picture to be smaller
4. I think it would be fine to allow that feature only for configurable roles. How can I test if a user has a special role? How about creating a special permission and apply that to rules specified in settings? But if I use the configurationmodel in settings, how can I react on a change (which I would have to do if I use permissionmodel)?
5. I hook base_render_before and test if Sender is the ProfileController. Isn't there a better hook if I want to do something whenever a profile is accessed?
6. I think a logo would be fine once the plugin is finished... ;)

Yes, I admit I'm a bit lazy. I haven't looked at other plugins to find my answers, but I'm a little bit short of time and would like to finish that plugin soon.

Many thanks for any help!

«1

Comments

  • Number 4. Register a permission and just check for that permission in your hooks, no need to make it configurable in the settings page. Number 5. Use ProfileController_Render_Before

    The rest will have to wait until I can actually look at the plugin code :D

    This sounds like a cool plugin!

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

  • peregrineperegrine MVP
    edited December 2013

    @hgtonight

    This sounds like a cool plugin!

    cool to code but ...
    sounds a bit like eavesdropping to me and personally would turn me off, if everything I viewed is monitored AND displayed. But thats me, I'm sure others will love it.

    you could try hgtonights suggestion if that works for you or you could

    try this for #5

    you could replace

       /**
        * Adds visit to db and increase visit counter
        * 
        * @param Controller $Sender The object calling this method
        * @return void
        */
       public function Base_Render_Before($Sender) {
          // only do this for Profile Controller
          if (get_class($Sender) != 'ProfileController') {
             return;
          }
          // if user is looking at own profile, just update count, then exit
          $UserID = Gdn::Session()->UserID;
          $ProfileUserID = $Sender->User->UserID;
          if ($UserID == $ProfileUserID || $ProfileUserID == '') {
             ProfileVisitorsModel::SetCount($ProfileUserID);
             return;
          }
          // save visit to db
          ProfileVisitorsModel::SaveVisit($ProfileUserID, $UserID);
          // update visitor counter in table user
          ProfileVisitorsModel::SetCount($ProfileUserID);
       }
    

    with this and you would actually get the ProfileUserID and act only once.

     public function ProfileController_BeforeRenderAsset_Handler($Sender) {
    
         $AssetName = GetValueR('EventArguments.AssetName', $Sender);
    
            if ($AssetName != "Content")
                return;
    
          // if user is looking at own profile, just update count, then exit     
         $UserID = Gdn::Session()->UserID;
          $ProfileUserID = $Sender->User->UserID;
          if ($UserID == $ProfileUserID || $ProfileUserID == '') {
             ProfileVisitorsModel::SetCount($ProfileUserID);
             return;
          }
          // save visit to db
          ProfileVisitorsModel::SaveVisit($ProfileUserID, $UserID);
          // update visitor counter in table user
          ProfileVisitorsModel::SetCount($ProfileUserID);
       }
    

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • you could also use public function ProfileController_AfterUserInfo_Handler($Sender) {

    to display profile visitors

    and use the profile visitor link for your settings.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • I really like the idea of this plugin. When I was working in a big company with an internal intranet portal we used to have something similar for our 'employee profile'. Really fun and more interactive.

    What if user1 looks at profile of user2 and then few days later comes back and looks again? Another DB entry or an updated DB entry?

    What do our DBA's say about it?

    There was an error rendering this rich post.

  • peregrineperegrine MVP
    edited December 2013

    if it were me I would store the date and update the date each time and keep one entry per looker

    I would avoid this - lets say someone is debugging their forum and disabling your plugin temporarily - they lose all their data.

     public function OnDisable() {
          // remove all config entries
          RemoveFromConfig('Plugins.ProfileVisitors');
          // drop tables
          Gdn::Structure()->Table('Profilevisitor')->Drop();
          Gdn::Structure()->Table('User')->DropColumn('CountProfilevisitors');
       }
    

    I would just entirely remove the OnDisable() function and not delete or drop anything.

    what you could do is put a cleanup function if your desire is to clean up data if your plugin is no longer used.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • Many thanks for the input right now! I hope I can implement that on the weekend.

    @UnderDog: there is only one row for each visitor/visited. So only the time get's updated. I've found two great functions in Garden for exactly that purpose

       public function SaveVisit($ProfileUserID, $UpdateUserID) {
          GDN::SQL()
             ->History(TRUE, FALSE)
             ->Replace('Profilevisitor',
                array('ProfileUserID' => $ProfileUserID, 'UpdateUserID' => $UpdateUserID),
                array('ProfileUserID' => $ProfileUserID, 'UpdateUserID' => $UpdateUserID)
             );
       }
    

    If you have a table that has the columns UpdateUserID and DateUpdated, function History(TRUE, FALSE) will fill those fields with session user and timestamp!
    It will also set fields InsertUserID and DateInserted if they exist and if you use History(TRUE, TRUE). I've changed my original column naming scheme as soon as I've found that function ;)
    Function Replace does an insert or an update depending on the existence of a row given by the criteria.

    @peregrine: yes, all data will be lost on disable but I don't think that keeping them is a better solution. Both are crappy.
    I would love to have an export feature for plugin settings. Wouldn't it be great to have an [Export] and an [Import] button in your plugin screen that will make an export/import of all your config settings and db changes?
    Well, it could be implemented, but I haven't seen any help for something like that in the framework right now. Something like functions those functions would be great:
    ExportColumn(tablename, array(columns))
    ExportTable(tablename)
    ExportSettings(Plugins.PluginName)
    ImportColumn(tablename, array(columns), overwrite:yes|no)
    ImportTable(tablename, overwrite:yes|no)
    ImportSettings(Plugins.PluginName, overwrite:yes|no)
    All those Export and Import functions must be wrapped into one function each and that would take a file as a parameter. So an admin could export and import plugin settings easily. Well, most things can be done by a simple table backup in phpmysql so it's questionable if such an effort will be worth it.

    Another great way would be a two step disabling process: admin hits [Disable] and plugin author could prompt a question and start a function depending on that question. "Do you want to clean up each entry of that plugin? Yes|No" => if yes, drop columns and tables...

  • @R_J said:
    Wouldn't it be great to have an [Export] and an [Import] button in your plugin screen that will make an export/import of all your config settings and db changes?

    LOL! I know a certain 'Don Peregrino' that would be looking to rebuild the Porter Plugin for exactly that purpose, heheh

    There was an error rendering this rich post.

  • @peregrine: not tested yet, but I think I will use something like this in order to solve the purge/keep data dilemma:

       public function OnDisable() {
          if (C('Plugins.ProfileVisitors.PurgeOnDisable') = 1) {
             // remove all config entries
             ...
             // drop tables
             ...
          }
    

    I add an option for that in the settings menu. I'm not sure about the default setting, but I tend to follow your preference and set it to unchecked so that data will not be deleted by accident.

    By the way: I've looked if it is possible to hook the disabling process in order to show a dialog, but there is only a hook after the plugin has been disabled: AddonDisabled. That's too late...
    I think I'll ask for a BeforeAddonDisabled via GitHub Pull request. That way it would be possible to face the admin with a dialogue and ask him to keep or to purge data.

  • That said, I've looked closer at GitHub and now I've seen I wasn't looking at the master branch :-/
    There isn't even a function OnDisable in V2.1b2! Argh...

  • peregrineperegrine MVP
    edited December 2013

    There isn't even a function OnDisable in V2.1b2! Argh..

    r_u sure r_j ?

    it appears to be:
    
     class.pluginmanager.php
    
     private function _PluginHook($PluginName, $ForAction, $Callback = FALSE) {
    
          switch ($ForAction) {
             case self::ACTION_ENABLE:  $HookMethod = 'Setup'; break;
             case self::ACTION_DISABLE: $HookMethod = 'OnDisable'; break;
             //case self::ACTION_REMOVE:  $HookMethod = 'CleanUp'; break;
             case self::ACTION_ONLOAD:  $HookMethod = 'OnLoad'; break;
          }
    
    
    an event could go here  in the call back:
    
        $Plugin->$HookMethod();
    

    I like the config option. You could even put it in the readme, instead of settings.

                this will always be true:
                  if (C('Plugins.ProfileVisitors.PurgeOnDisable') = 1) 
    

    it needs to be ==

              if (C('Plugins.ProfileVisitors.PurgeOnDisable') == 1) 
    

    or better yet I think

    if (C('Plugins.ProfileVisitors.PurgeOnDisable') === TRUE)

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • It is just not called OnDisable, but yes, it exists.
    Thanks for the hint concerning the equal sign. I ran into difficulties with TRUE and FALSE one time but I think it is because I haven't really understood the difference between == and === by now...

  • I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

  • But then I prefer if (C('Plugins.ProfileVisitors.PurgeOnDisable') == TRUE) because I've seen TRUE and FALSE saved as 1 and 0 and I want to be fail safe.

  • Creating a profile view that looks like a native view was exceptionally hard for me! That's why I'll explain what I've done and - whenever I can ;) - why I have done what I have done.
    First the complete code:

       public function ProfileController_Visitors_Create($Sender) {
          // don't show edit profile menu
          $Sender->EditMode(FALSE); 
          // get user id
          $Session = Gdn::Session();
          $UserID = $Session->User->UserID;
          // set user info
          $Sender->GetUserInfo('', '', $UserID, TRUE);
    
          $Sender->SetData('Title', T('Profile Visitors'));
          $Sender->SetData('Breadcrumbs', array(
                array('Name' => T('Profile'), 'Url' => '/profile'),
                array('Name' => T('Visitors'), 'Url' => '/profile/visitors'))
          );
          // get list of visitors from db
          $Sender->SetData('Visitors', ProfileVisitorsModel::Get($UserID, $Limit));
    
          // render view
          $MyView = $this->GetView('visitors.php');
          $Sender->SetTabView(T('Profile Visitors'), $MyView);
          $ProfileView = $Sender->FetchViewLocation('index', 'ProfileController', 'dashboard');
          $Sender->Render($ProfileView);
       }
    

    I've only left over the lines you need to show a profile page, full source code is on GitHub.

    1. Line: 3: EditMode will decide if you see the navigation list for "Change Password", "Notification Preferences", etc or not. I wanted to inform and no input, so I set it to false.
    2. Lines 5-8: You'll need to add the profile user (at least for SetTabView, see below)
    3. Lines 10-16: Your page should contain a title, display breadcrumbs and have any data for the view to show. Information is accessable in view with $this->Data('Title')
    4. Line 19: get the path of your plugins view
    5. Line 20: SetTabView marks in the profiles navigation list the current tab and also stores the view you want to display
    6. Line 21: The index function of the profile controller is a wrapper for a) user information on top of the page and b) the view, stored with SetTabView, below the user information.
    7. Render ProfileControllers index function

    I assume most of you developers have implemented something like that in Garden before but I do not like searching for answers in old threads and only finding questions. So I wanted to complete this thread.

    @hgtonight: 'RegisterPermissions' => array('Plugins.ProfileVisitors.View' => 1) did exactly what I needed

    @peregrine: I've used ProfileController_BeforeRenderAsset_Handler, works perfectly.

    Now the only thing I have to solve is the CSS question, but that's the smallest problem :)

    (Offtopic: have you noticed that the smiling smiley looks way more excited than the grinning one? :) vs. :D Somewhat irritating to me)

  • I just have a few questions.

    You currently override the profile user in /profile/visitors to be the session user but link to /profile/visitors/{userID}/{username}. This means I can visit anyone's profile visits link and will be shown my own profile/visits. Is this the intended effect?

    Is there a specific reason you chose to hook into the BeforeRenderAsset event rather than the Render_Before event? I only ask because you don't seem to want to tally multiple views for each asset. You even filter it down so it only tallies on the content asset.

    Did you want each user to be able to configure their notifications? I see you have it listed as a todo at top. If you want to add in a notification preference, use something like below:

    public function ProfileController_AfterPreferencesDefined_Handler($Sender) {
      $Sender->Preferences['Notifications']['Popup.ProfileVisitors'] = T('Notify me when people visit my profile.');
      // Doesn't seem like you would want an email notification, but this is how to add it anyway
      // $Sender->Preferences['Notifications']['Email.ProfileVisitors'] = T('Notify me when people visit my profile.');
    }
    

    This will add an option on the notification preferences page. If you use the activity model to handle notifications, these preferences will be taken into account automagically.

    I am a little confused as to what the CSS issue is. Mind clarifying?

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

  • R_JR_J Admin
    edited December 2013

    @hgtonight said:
    This means I can visit anyone's profile visits link and will be shown my own profile/visits. Is this the intended effect?

    Nope, that's sloppy coded. Thanks for pointing me to it.

    Is there a specific reason you chose to hook into the BeforeRenderAsset event rather than the Render_Before event? I only ask because you don't seem to want to tally multiple views for each asset. You even filter it down so it only tallies on the content asset.

    Lazyness. I've just copied without thinking...

    Did you want each user to be able to configure their notifications? I see you have it listed as a todo at top. If you want to add in a notification preference, use something like below:

    public function ProfileController_AfterPreferencesDefined_Handler($Sender) {
      $Sender->Preferences['Notifications']['Popup.ProfileVisitors'] = T('Notify me when people visit my profile.');
      // Doesn't seem like you would want an email notification, but this is how to add it anyway
      // $Sender->Preferences['Notifications']['Email.ProfileVisitors'] = T('Notify me when people visit my profile.');
    }
    

    This will add an option on the notification preferences page. If you use the activity model to handle notifications, these preferences will be taken into account automagically.

    Whoa that's great! I was thinking about adding such a notification. If it is tracked, why not notify when it happens? But I would deactivate it by default because it seems to be a little bit silly to me. It would just be for completeness.

    I am a little confused as to what the CSS issue is. Mind clarifying?

    Take a look at the first screenshot: there you can see how activity aligns it's postings so that there seems to be an extra column for user pictures.
    If you look at the screenshot of my profile visitor list, you see that the picture destroys the layout. I think I would like to have like it is in the activity stream. Maybe with an additional odd/even color.

    Thanks for looking so thoroughly at my code. Your findings are a little bit embarrassing for me, but very, very welcome!

  • Thanks for the clarification.

    You can get the same effect by adding the class 'Activities' to your data list element. As far as the user photos being contained, the activity view cheats a little since the activity is always as tall or taller than the user's photo.

    @R_J said:
    Thanks for looking so thoroughly at my code. Your findings are a little bit embarrassing for me, but very, very welcome!

    Feeling embarrassed is a good thing in my book. Means you are humble and open to criticism. That said, I didn't mean to embarrass. There are many ways to code a cat, after all. :D

    P.S. I submitted a PR on github with these changes in place.

    Search first

    Check out the Documentation! We are always looking for new content and pull requests.

    Click on insightful, awesome, and funny reactions to thank community volunteers for their valuable posts.

  • Great. Asked for an event that I can hook when a user presses a button that calls a function of my plugin

    @hgtonight: I will try your code tomorrow. I've already thought about adding a   below each line to make it as tall as lines with profile pictures but thought that should be possible with css. But if you've already made a pull request, I assume you already have solved my problem ;)

  • r_j

    I don't know if you were referring to this being too late or "how it would be used"

    but there is a

    $this->FireEvent('AfterDisablePlugin'); in settingscontroller.php in vanilla 2.1b2

    it probably won't be insightful if you are already aware, but in case you aren't.

    I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.

Sign In or Register to comment.