#868 Implement pinned projects feature
Closed 4 years ago by praiskup. Opened 4 years ago by frostyx.
copr/ frostyx/copr pinned-projects  into  master

@@ -0,0 +1,36 @@ 

+ """

+ Add table for PinnedCoprs

+ 

+ Revision ID: 55a07cb7bd68

+ Revises: 2d8b4722918b

+ Create Date: 2019-06-24 22:18:20.411614

+ """

+ 

+ import sqlalchemy as sa

+ from alembic import op

+ 

+ 

+ revision = '55a07cb7bd68'

+ down_revision = '1f4e04bb3618'

+ 

+ 

+ def upgrade():

+     op.create_table('pinned_coprs',

+         sa.Column('id', sa.Integer(), nullable=False),

+         sa.PrimaryKeyConstraint('id'),

+ 

+         sa.Column('copr_id', sa.Integer(), nullable=True),

+         sa.Column('user_id', sa.Integer(), nullable=True),

+         sa.Column('group_id', sa.Integer(), nullable=True),

+         sa.Column('position', sa.Integer(), nullable=False),

+ 

+         sa.ForeignKeyConstraint(['copr_id'], ['copr.id'], ),

+         sa.ForeignKeyConstraint(['group_id'], ['group.id'], ),

+         sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),

+     )

+     op.create_index(op.f('ix_pinned_coprs_user_id'), 'pinned_coprs', ['user_id'], unique=False),

+     op.create_index(op.f('ix_pinned_coprs_group_id'), 'pinned_coprs', ['group_id'], unique=False),

+ 

+ 

+ def downgrade():

+     op.drop_table('pinned_coprs')

@@ -87,6 +87,9 @@ 

      # set this to {"epel-8": "rhelbeta-8"}

      CHROOT_NAME_RELEASE_ALIAS = {}

  

+     # How many pinned projects a user or group can have

+     PINNED_PROJECTS_LIMIT = 4

+ 

  

  class ProductionConfig(Config):

      DEBUG = False

@@ -14,6 +14,7 @@ 

      from flask_wtf import Form as FlaskForm

  

  from coprs import constants

+ from coprs import app

  from coprs import helpers

  from coprs import models

  from coprs.logic.coprs_logic import CoprsLogic, MockChrootsLogic
@@ -1098,6 +1099,27 @@ 

      upload_comps = FileField("Upload comps.xml")

      delete_comps = wtforms.BooleanField("Delete comps.xml", false_values=FALSE_VALUES)

  

+ 

+ class PinnedCoprsForm(FlaskForm):

+     copr_ids = wtforms.SelectMultipleField(wtforms.IntegerField("Pinned Copr ID"))

+ 

+     def validate(self):

+         if any([i and not i.isnumeric() for i in self.copr_ids.data]):

+             self.errors["coprs"] = ["Unexpected value selected"]

+             return False

+ 

+         limit = app.config["PINNED_PROJECTS_LIMIT"]

+         if len(self.copr_ids.data) > limit:

+             self.errors["coprs"] = ["Too many pinned projects. Limit is {}!".format(limit)]

+             return False

+ 

+         if len(list(filter(None, self.copr_ids.data))) != len(set(filter(None, self.copr_ids.data))):

+             self.errors["coprs"] = ["You can pin a particular project only once"]

+             return False

+ 

+         return True

+ 

+ 

  class AdminPlaygroundForm(FlaskForm):

      playground = wtforms.BooleanField("Playground", false_values=FALSE_VALUES)

  

@@ -349,6 +349,18 @@ 

      return url_for(view, username=copr.user.name, coprname=copr.name, **kwargs)

  

  

+ def owner_url(owner):

+     """

+     For a given `owner` object, which may be either `models.User` or `models.Group`,

+     return an URL to its _profile_ page.

+     """

+     # We can't check whether owner is instance of `models.Group` because once

+     # we include models from helpers, we get circular imports

+     if hasattr(owner, "at_name"):

+         return url_for("groups_ns.list_projects_by_group", group_name=owner.name)

+     return url_for("coprs_ns.coprs_by_user", username=owner.username)

+ 

+ 

  def url_for_copr_view(view, group_view, copr, **kwargs):

      if copr.is_a_group_project:

          return url_for(group_view, group_name=copr.group.name, coprname=copr.name, **kwargs)

@@ -212,6 +212,16 @@ 

              running=running,

          )

  

+     @classmethod

+     def get_coprs_permissible_by_user(cls, user):

+         coprs = CoprsLogic.filter_without_group_projects(

+                     CoprsLogic.get_multiple_owned_by_username(

+                         flask.g.user.username, include_unlisted_on_hp=False)).all()

+ 

+         for group in user.user_groups:

+             coprs.extend(CoprsLogic.get_multiple_by_group_id(group.id).all())

+ 

+         return coprs

  

  

  class ProjectForking(object):

@@ -136,8 +136,8 @@ 

  

      # user_relation="owned", username=username, with_mock_chroots=False

      @classmethod

-     def get_multiple_owned_by_username(cls, username):

-         query = cls.get_multiple()

+     def get_multiple_owned_by_username(cls, username, include_unlisted_on_hp=True):

+         query = cls.get_multiple(include_unlisted_on_hp=include_unlisted_on_hp)

          return query.filter(models.User.username == username)

  

      @classmethod
@@ -159,6 +159,10 @@ 

          return query.filter(models.Copr.group_id.is_(None))

  

      @classmethod

+     def filter_without_ids(cls, query, ids):

+         return query.filter(models.Copr.id.notin_(ids))

+ 

+     @classmethod

      def join_builds(cls, query):

          return (query.outerjoin(models.Copr.builds)

                  .options(db.contains_eager(models.Copr.builds))
@@ -874,3 +878,42 @@ 

                  chroots[chroot.name] = chroot.is_active

  

          return chroots

+ 

+ 

+ class PinnedCoprsLogic(object):

+ 

+     @classmethod

+     def get_all(cls):

+         return db.session.query(models.PinnedCoprs).order_by(models.PinnedCoprs.position)

+ 

+     @classmethod

+     def get_by_id(cls, pin_id):

+         return cls.get_all().filter(models.PinnedCoprs.id == pin_id)

+ 

+     @classmethod

+     def get_by_owner(cls, owner):

+         if isinstance(owner, models.Group):

+             return cls.get_by_group_id(owner.id)

+         return cls.get_by_user_id(owner.id)

+ 

+     @classmethod

+     def get_by_user_id(cls, user_id):

+         return cls.get_all().filter(models.PinnedCoprs.user_id == user_id)

+ 

+     @classmethod

+     def get_by_group_id(cls, group_id):

+         return cls.get_all().filter(models.PinnedCoprs.group_id == group_id)

+ 

+     @classmethod

+     def add(cls, owner, copr_id, position):

+         kwargs = dict(copr_id=copr_id, position=position)

+         kwargs["group_id" if isinstance(owner, models.Group) else "user_id"] = owner.id

+         pin = models.PinnedCoprs(**kwargs)

+         db.session.add(pin)

+ 

+     @classmethod

+     def delete_by_owner(cls, owner):

+         query = db.session.query(models.PinnedCoprs)

+         if isinstance(owner, models.Group):

+             return query.filter(models.PinnedCoprs.group_id == owner.id).delete()

+         return query.filter(models.PinnedCoprs.user_id == owner.id).delete()

@@ -195,6 +195,22 @@ 

              return ""

  

  

+ class PinnedCoprs(db.Model, helpers.Serializer):

+     """

+     Representation of User or Group <-> Copr relation

+     """

+     id = db.Column(db.Integer, primary_key=True)

+ 

+     copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"))

+     user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True, index=True)

+     group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=True, index=True)

+     position = db.Column(db.Integer, nullable=False)

+ 

+     copr = db.relationship("Copr")

+     user = db.relationship("User")

+     group = db.relationship("Group")

+ 

+ 

  class _CoprPublic(db.Model, helpers.Serializer, CoprSearchRelatedData):

      """

      Represents public part of a single copr (personal repo with builds, mock

@@ -684,3 +684,40 @@ 

        </tbody>

      </table>

  {% endmacro %}

+ 

+ 

+ {% macro render_project_box(copr, pinned=False) %}

+ <!--copr-project-->

+ <a href="{{ copr_details_href(copr) }}" class="list-group-item">

+   <div>

+     <h3 class="list-group-item-heading" style="display: inline;">

+       {{ copr_name(copr) }}

+     </h3>

+     {% if copr.delete_after %}

+     <small> (temporary project, {{ copr.delete_after_msg }})</small>

+     {% endif %}

+   </div>

+ 

+   {% if pinned %}

+   <span class="pull-right" title="Pinned project">

+     <i class="fa fa-thumb-tack fa-lg" aria-hidden="true"></i>

+   </span>

+   {% endif %}

+ 

+   <span class="list-group-item-text">

+       {{ copr.description|markdown|remove_anchor|default('Description not filled in by author. Very likely personal repository for testing purpose, which you should not use.', true) }}

+       <ul class="list-inline text-muted">

+       {% for os in copr.active_chroots_grouped %}

+         <li>

+           <strong>{{ friendly_os_name(os[0].split()[0], os[0].split()[1]) }}:</strong>

+           <small>

+           {% for arch in os[1] %}

+             {{ arch }}{% if not loop.last %}, {% endif %}

+           {% endfor %}

+           </small>

+         </li>

+       {% endfor %}

+       </ul>

+     </span>

+ </a>

+ {% endmacro %}

@@ -2,39 +2,24 @@ 

  {% block title %}Project List{% endblock %}

  {% block header %}Project List{% endblock %}

  {% from "_helpers.html" import render_pagination, copr_details_href, copr_name, user_projects_panel %}

- {% from "_helpers.html" import recent_builds_panel, task_queue_panel, friendly_os_name %}

+ {% from "_helpers.html" import recent_builds_panel, task_queue_panel, friendly_os_name, render_project_box %}

  {%block main_menu_projects %}active{% endblock %}

  {% block body %}

  <div class="row">

    <div class="col-md-9 col-sm-8">

      {% block show_top %}

      {% endblock %}

-     <div class="list-group">

-     {% for copr in coprs %}

-       <!--copr-project-->

-       <a href="{{ copr_details_href(copr) }}" class="list-group-item">

-         <h3 class="list-group-item-heading">

-           {{ copr_name(copr) }}

-         </h3>

-         <span class="list-group-item-text">

-           {{ copr.description|markdown|remove_anchor|default('Description not filled in by author. Very likely personal repository for testing purpose, which you should not use.', true) }}

-           <ul class="list-inline text-muted">

-           {% for os in copr.active_chroots_grouped %}

-             <li>

-               <strong>{{ friendly_os_name(os[0].split()[0], os[0].split()[1]) }}:</strong>

-               <small>

-               {% for arch in os[1] %}

-                 {{arch}}{% if not loop.last %}, {% endif %}

-               {%endfor%}

-               </small>

-             </li>

-           {% endfor %}

-           </ul>

-         </span>

-       </a>

-     {% else %}

-       <p>No projects...</p>

-     {% endfor %}

+ 

+     {% block projects_header %}

+     {% endblock %}

+     <div class="panel panel-default">

+       <div class="list-group">

+       {% for copr in pinned + coprs %}

+         {{ render_project_box(copr, pinned = copr in pinned) }}

+       {% else %}

+         <p>No projects...</p>

+       {% endfor %}

+       </div>

      </div>

      {{ render_pagination(request, paginator) }}

    </div>

@@ -3,6 +3,7 @@ 

  {% block header %}Project List{% endblock %}

  {% from "_helpers.html" import render_pagination, copr_details_href, copr_name, user_projects_panel %}

  {% from "_helpers.html" import recent_builds_panel, task_queue_panel, recent_blog_panel, friendly_os_name %}

+ {% from "_helpers.html" import render_project_box %}

  {%block main_menu_projects %}active{% endblock %}

  {% block body %}

  <div class="row">
@@ -10,35 +11,14 @@ 

    {% block content %}

      {% block show_top %}

      {% endblock %}

+ 

+     {% block projects_header %}

+     {% endblock %}

+ 

      <div class="panel panel-default">

        <div class="list-group">

-       {% for copr in coprs %}

-         <!--copr-project-->

-         <a href="{{ copr_details_href(copr) }}" class="list-group-item">

-           <div>

-           <h3 class="list-group-item-heading" style="display: inline;">

-               {{ copr_name(copr) }}

-           </h3>

-           {% if copr.delete_after %}

-           <small> (temporary project, {{ copr.delete_after_msg }})</small>

-           {% endif %}

-           </div>

-           <span class="list-group-item-text">

-             {{ copr.description|markdown|remove_anchor|default('Description not filled in by author. Very likely personal repository for testing purpose, which you should not use.', true) }}

-             <ul class="list-inline text-muted">

-             {% for os in copr.active_chroots_grouped %}

-               <li>

-                 <strong>{{ friendly_os_name(os[0].split()[0], os[0].split()[1]) }}:</strong>

-                 <small>

-                 {% for arch in os[1] %}

-                   {{ arch }}{% if not loop.last %}, {% endif %}

-                 {% endfor %}

-                 </small>

-               </li>

-             {% endfor %}

-             </ul>

-           </span>

-         </a>

+       {% for copr in pinned + coprs %}

+         {{ render_project_box(copr, pinned=copr in pinned) }}

        {% else %}

          <p>No projects...</p>

        {% endfor %}

@@ -1,8 +1,9 @@ 

  {% extends "coprs/show.html" %}

  {% block title %}Project List{% endblock %}

  {% block header %}Project List{% endblock %}

- {% block show_top %}

  

+ 

+ {% block show_top %}

  {% if not g.user and not fulltext%}

  <br>

  <div class="panel panel-default">
@@ -11,7 +12,10 @@ 

    </div>

  </div>

  {% endif %}

+ {% endblock %}

+ 

  

+ {% block projects_header %}

  {% if g.user %}

  <a href="{{url_for('coprs_ns.copr_add', username=g.user.name) }}" class="btn button-new pull-right btn-primary">

    <span class="pficon pficon-add-circle-o"></span> New Project

@@ -14,5 +14,7 @@ 

  </ol>

  {% endblock %}

  {% block show_top %}

+ 

+ 

  <h1> Search Results <small> {{fulltext}} </small></h1>

  {% endblock %}

@@ -12,21 +12,31 @@ 

    </li>

  </ol>

  {% endblock %}

- {% block show_top %}

  

+ 

+ {% block show_top %}

  <div id="profile">

    <h1>@{{group.name}} Group</h1>

    <p>

      <a href="https://admin.fedoraproject.org/accounts/group/view/{{ group.fas_name }}" title="{{ group.fas_name }}'s FAS details" target="_blank">FAS details</a> |

      <a href="https://admin.fedoraproject.org/accounts/group/members/{{ group.fas_name }}" title="{{ group.fas_name }}'s Members" target="_blank">View Members</a>

-   </h>

+   </p>

  </div>

+ {% endblock %}

  

+ 

+ {% block projects_header %}

  {% if g.user and g.user.can_build_in_group(group) %}

- <a href="{{url_for('coprs_ns.copr_add', group_name=group.name) }}" class="btn button-new pull-right btn-primary">

-   <span class="pficon pficon-add-circle-o"></span>

-     New Group Project

- </a>

+ <div class="btn-group pull-right" role="group" aria-label="Basic example">

+   <a href="{{url_for('coprs_ns.copr_add', group_name=group.name) }}" class="btn button-new btn-primary">

+     <span class="pficon pficon-add-circle-o"></span>

+       New Group Project

+   </a>

+ 

+   <a href="{{ url_for('user_ns.pinned_projects', group_name=group.name) }}" class="btn button-new btn-secondary">

+     <span class="pficon pficon-edit"></span> Customize pinned

+   </a>

+ </div>

  {% endif %}

  

  <h2 class="page-title">Projects in @{{group.name}} Group</h2>

@@ -12,8 +12,9 @@ 

    </li>

  </ol>

  {% endblock %}

- {% block show_top %}

  

+ 

+ {% block show_top %}

  <div id="profile">

    <img src="{{ user.gravatar_url }}" alt="User Image" class="avatar">

    <h1>{{user.name|capitalize}}'s Profile</h1>
@@ -37,11 +38,21 @@ 

        {% include "user_meta.html" %}

    </p>

  </div>

+ {% endblock %}

  

+ 

+ {% block projects_header %}

  {% if g.user == user %}

- <a href="{{url_for('coprs_ns.copr_add', username=g.user.name) }}" class="btn button-new pull-right btn-primary">

-   <span class="pficon pficon-add-circle-o"></span> New Project

- </a>

+ <div class="btn-group pull-right" role="group" aria-label="Basic example">

+ 

+   <a href="{{url_for('coprs_ns.copr_add', username=g.user.name) }}" class="btn button-new btn-primary">

+     <span class="pficon pficon-add-circle-o"></span> New Project

+   </a>

+ 

+   <a href="{{ url_for('user_ns.pinned_projects') }}" class="btn button-new btn-secondary">

+     <span class="pficon pficon-edit"></span> Customize pinned

+   </a>

+ </div>

  {% endif %}

  

  <h2 class="page-title">{{user.name|capitalize}}'s Projects</h2>

@@ -0,0 +1,74 @@ 

+ {% extends "coprs/show.html" %}

+ {% from "_helpers.html" import render_form_errors %}

+ {% block title %}Customize pinned projects{% endblock %}

+ {% block header %}Customize pinned projects{% endblock %}

+ {% block breadcrumbs %}

+ <ol class="breadcrumb">

+     <li>

+         <a href="{{ url_for('coprs_ns.coprs_show') }}">Home</a>

+     </li>

+     <li>

+       {% if owner.at_name is defined %}

+         <a href="{{ url_for('groups_ns.list_projects_by_group', group_name=owner.name) }}">{{ owner.at_name }}</a>

+       {% else %}

+         <a href="{{ url_for('coprs_ns.coprs_by_user', username=owner.name) }}">{{ owner.name }}</a>

+       {% endif %}

+     </li>

+     <li class="active">

+         Customize pinned projects

+     </li>

+ </ol>

+ {% endblock %}

+ 

+ {% block content %}

+ <h1 style="margin-bottom:22px;margin-top:22px">Pinned projects</h1>

+ 

+ {% if form %}

+   {{ render_form_errors(form=form) }}

+ {% endif %}

+ 

+ <form action="" method="POST">

+   <div class="panel panel-default">

+ 

+     <div class="panel-heading">

+       Customize pinned projects for

+       {{ owner.at_name if owner.at_name is defined else owner.name }}

+     </div>

+ 

+     <div class="panel-body">

+       <p>

+         Configure up to four pinned projects, that you are particularly proud of or recognized for. They will be displayed

+         on the top of your user/group page the exact order. It is possible to select your personal projects, group

+         projects and even someone else's projects, that you have permissions for.

+       </p>

+     </div>

+ 

+     <table class="table table-bordered">

+       <thead>

+         <tr>

+           <th>Slot</th>

+           <th>Project</th>

+         </tr>

+       </thead>

+ 

+       <tbody>

+       {% for i in range(0, config.PINNED_PROJECTS_LIMIT) %}

+         <tr>

+           <td>#{{ i + 1 }}</td>

+           <td>

+             <select name="copr_ids" class="input">

+               <option value="">Nothing</option>

+               {% for copr in coprs %}

+                 {% set selected = 'selected' if copr.id == selected[i] else '' %}

+                 <option value="{{ copr.id }}" {{ selected }}>{{ copr.full_name }}</option>

+               {% endfor %}

+             </select>

+           </td>

+         </tr>

+       {% endfor %}

+       </tbody>

+     </table>

+     <input class="btn btn-primary pull-right" type="submit" name="submit" value="Submit">

+   </div>

+ </form>

+ {% endblock %}

@@ -28,7 +28,7 @@ 

  from coprs import helpers

  from coprs import models

  from coprs.exceptions import ObjectNotFound

- from coprs.logic.coprs_logic import CoprsLogic

+ from coprs.logic.coprs_logic import CoprsLogic, PinnedCoprsLogic

  from coprs.logic.stat_logic import CounterStatLogic

  from coprs.logic.modules_logic import ModulesLogic, ModulemdGenerator, ModuleBuildFacade

  from coprs.rmodels import TimedStatEvents
@@ -77,6 +77,7 @@ 

  

      return flask.render_template("coprs/show/all.html",

                                   coprs=coprs,

+                                  pinned=[],

                                   paginator=paginator,

                                   tasks_info=ComplexLogic.get_queue_sizes(),

                                   users_builds=users_builds,
@@ -91,12 +92,13 @@ 

          return page_not_found(

              "User {0} does not exist.".format(username))

  

+     pinned = [pin.copr for pin in PinnedCoprsLogic.get_by_user_id(user.id)] if page == 1 else []

      query = CoprsLogic.get_multiple_owned_by_username(username)

+     query = CoprsLogic.filter_without_ids(query, [copr.id for copr in pinned])

      query = CoprsLogic.filter_without_group_projects(query)

      query = CoprsLogic.set_query_order(query, desc=True)

  

      paginator = helpers.Paginator(query, query.count(), page)

- 

      coprs = paginator.sliced_query

  

      # flask.g.user is none when no user is logged - showing builds from everyone
@@ -107,6 +109,7 @@ 

      return flask.render_template("coprs/show/user.html",

                                   user=user,

                                   coprs=coprs,

+                                  pinned=pinned,

                                   paginator=paginator,

                                   tasks_info=ComplexLogic.get_queue_sizes(),

                                   users_builds=users_builds,
@@ -132,6 +135,7 @@ 

      coprs = paginator.sliced_query

      return render_template("coprs/show/fulltext.html",

                              coprs=coprs,

+                             pinned=[],

                              paginator=paginator,

                              fulltext=fulltext,

                              tasks_info=ComplexLogic.get_queue_sizes(),

@@ -7,12 +7,13 @@ 

  from coprs.helpers import Paginator

  from coprs.logic import builds_logic

  from coprs.logic.complex_logic import ComplexLogic

- from coprs.logic.coprs_logic import CoprsLogic

+ from coprs.logic.coprs_logic import CoprsLogic, PinnedCoprsLogic

  from coprs.logic.users_logic import UsersLogic

  from coprs import app

  

  from ... import db

  from ..misc import login_required

+ from ..user_ns import user_general

  

  from . import groups_ns

  
@@ -60,10 +61,11 @@ 

  @groups_ns.route("/g/<group_name>/coprs/<int:page>")

  def list_projects_by_group(group_name, page=1):

      group = ComplexLogic.get_group_by_name_safe(group_name)

-     query = CoprsLogic.get_multiple_by_group_id(group.id)

  

+     pinned = [pin.copr for pin in PinnedCoprsLogic.get_by_group_id(group.id)] if page == 1 else []

+     query = CoprsLogic.get_multiple_by_group_id(group.id)

+     query = CoprsLogic.filter_without_ids(query, [copr.id for copr in pinned])

      paginator = Paginator(query, query.count(), page)

- 

      coprs = paginator.sliced_query

  

      data = builds_logic.BuildsLogic.get_small_graph_data('30min')
@@ -72,6 +74,7 @@ 

          "coprs/show/group.html",

          user=flask.g.user,

          coprs=coprs,

+         pinned=pinned,

          paginator=paginator,

          tasks_info=ComplexLogic.get_queue_sizes(),

          group=group,

@@ -1,9 +1,12 @@ 

  import flask

  from . import user_ns

+ from coprs import app, db, models, helpers

+ from coprs.forms import PinnedCoprsForm

  from coprs.views.misc import login_required

  from coprs.logic.users_logic import UsersLogic, UserDataDumper

  from coprs.logic.builds_logic import BuildsLogic

  from coprs.logic.complex_logic import ComplexLogic

+ from coprs.logic.coprs_logic import CoprsLogic, PinnedCoprsLogic

  

  

  def render_user_info(user):
@@ -37,3 +40,60 @@ 

      UsersLogic.delete_user_data(flask.g.user.username)

      flask.flash("Your data were successfully deleted.")

      return render_user_info(flask.g.user)

+ 

+ 

+ @user_ns.route("/customize-pinned/")

+ @user_ns.route("/customize-pinned/<group_name>")

+ @login_required

+ def pinned_projects(group_name=None):

+     owner = flask.g.user if not group_name else ComplexLogic.get_group_by_name_safe(group_name)

+     return render_pinned_projects(owner)

+ 

+ 

+ def render_pinned_projects(owner, form=None):

+     pinned = [pin.copr for pin in PinnedCoprsLogic.get_by_owner(owner)]

+     if isinstance(owner, models.Group):

+         UsersLogic.raise_if_not_in_group(flask.g.user, owner)

+         coprs = CoprsLogic.get_multiple_by_group_id(owner.id).filter(models.Copr.unlisted_on_hp.is_(False)).all()

+     else:

+         coprs = ComplexLogic.get_coprs_permissible_by_user(owner)

+     coprs = sorted(coprs, key=lambda copr: copr.full_name)

+     selected = [copr.id for copr in pinned]

+     selected += (app.config["PINNED_PROJECTS_LIMIT"] - len(pinned)) * [None]

+     for i, copr_id in enumerate(form.copr_ids.data if form else []):

+         selected[i] = int(copr_id) if copr_id else None

+ 

+     graph = BuildsLogic.get_small_graph_data('30min')

+     return flask.render_template("pinned.html",

+                                  owner=owner,

+                                  pinned=pinned,

+                                  selected=selected,

+                                  coprs=coprs,

+                                  form=form,

+                                  tasks_info=ComplexLogic.get_queue_sizes(),

+                                  graph=graph)

+ 

+ 

+ @user_ns.route("/customize-pinned/", methods=["POST"])

+ @user_ns.route("/customize-pinned/<group_name>", methods=["POST"])

+ @login_required

+ def pinned_projects_post(group_name=None):

+     owner = flask.g.user if not group_name else ComplexLogic.get_group_by_name_safe(group_name)

+     url_on_success = helpers.owner_url(owner)

+     return process_pinned_projects_post(owner, url_on_success)

+ 

+ 

+ def process_pinned_projects_post(owner, url_on_success):

+     if isinstance(owner, models.Group):

+         UsersLogic.raise_if_not_in_group(flask.g.user, owner)

+ 

+     form = PinnedCoprsForm()

+     if not form.validate_on_submit():

+         return render_pinned_projects(owner, form=form)

+ 

+     PinnedCoprsLogic.delete_by_owner(owner)

+     for i, copr_id in enumerate(filter(None, form.copr_ids.data)):

+         PinnedCoprsLogic.add(owner, int(copr_id), i)

+     db.session.commit()

+ 

+     return flask.redirect(url_on_success)

@@ -8,8 +8,9 @@ 

  

  from copr_common.enums import ActionTypeEnum

  from coprs import app

+ from coprs.forms import PinnedCoprsForm

  from coprs.logic.actions_logic import ActionsLogic

- from coprs.logic.coprs_logic import CoprsLogic, CoprChrootsLogic

+ from coprs.logic.coprs_logic import CoprsLogic, CoprChrootsLogic, PinnedCoprsLogic

  from coprs.logic.users_logic import UsersLogic

  

  from coprs import models
@@ -133,3 +134,36 @@ 

  

          # However, it should not be removed from the Copr

          assert [ch.name for ch in self.c2.copr_chroots] == ["fedora-17-x86_64", "fedora-17-i386"]

+ 

+ 

+ class TestPinnedCoprsLogic(CoprsTestCase):

+ 

+     def test_pinned_projects(self, f_users, f_coprs, f_db):

+         assert set(CoprsLogic.get_multiple_by_username(self.u2.name)) == {self.c2, self.c3}

+         assert set(PinnedCoprsLogic.get_by_owner(self.u2)) == set()

+ 

+         pc1 = models.PinnedCoprs(id=1, copr_id=self.c2.id, user_id=self.u2.id, position=1)

+         pc2 = models.PinnedCoprs(id=2, copr_id=self.c3.id, user_id=self.u2.id, position=2)

+         self.db.session.add_all([pc1, pc2])

+ 

+         assert set(PinnedCoprsLogic.get_by_owner(self.u2)) == {pc1, pc2}

+         assert set(CoprsLogic.get_multiple_by_username(self.u2.name)) == {self.c2, self.c3}

+ 

+     def test_limit(self):

+         app.config["PINNED_PROJECTS_LIMIT"] = 1

+         with app.app_context():

+             form = PinnedCoprsForm()

+             form.copr_ids.data = ["1"]

+             assert form.validate()

+ 

+             form.copr_ids.data = ["1", "2"]

+             assert not form.validate()

+             assert "Too many" in form.errors["coprs"][0]

+ 

+     def test_unique_coprs(self):

+         app.config["PINNED_PROJECTS_LIMIT"] = 2

+         with app.app_context():

+             form = PinnedCoprsForm()

+             form.copr_ids.data = ["1", "1"]

+             assert not form.validate()

+             assert "only once" in form.errors["coprs"][0]

See #495 for the specifications.

I am sorry, that it is so awfully long PR, but the feature turned out to be much more complicated than I've originally imagined.

I was unsure where and under what route URLs put some pieces of the code ... so if you find something on a totally different place than you would expect, please let me know, I will rework it.

[frontend] allow to set also group projects created by a different user
[frontend] render_project_box needs to generate also copr-project docstring
[frontend] specify the limit of pinned projects in the config
[frontend] allow groups to set their pinned projects
[frontend] when failure, select the submitted values
[frontend] implement pinned projects feature

Those look to me they should be squashed ... it is pretty difficult to review
them one-by-one.

Those look to me they should be squashed ...

No problem with that

5 new commits added

  • [frontend] implement pinned projects feature
  • [frontend] extract code for rendering project box into its own macro
  • [frontend] add missing border, same as user page has
  • [frontend] fix closing tag
  • [frontend] separate projects_header block from show_top block
4 years ago

Those look to me they should be squashed ...
No problem with that

Done

Is this really correct? Sounds like that if you only affect the first page, and not all the others - we'll miss some projects that would otherwise be on the first page.

Would you mind merging the user/group routes into one function? It seems to be possible.

Btw., what about to name this pinned-setup, instead of just pinned? (or some other name, which makes it clear that it is supposed to be private page)

5 new commits added

  • [frontend] implement pinned projects feature
  • [frontend] extract code for rendering project box into its own macro
  • [frontend] add missing border, same as user page has
  • [frontend] fix closing tag
  • [frontend] separate projects_header block from show_top block
4 years ago

5 new commits added

  • [frontend] implement pinned projects feature
  • [frontend] extract code for rendering project box into its own macro
  • [frontend] add missing border, same as user page has
  • [frontend] fix closing tag
  • [frontend] separate projects_header block from show_top block
4 years ago

Would you mind merging the user/group routes into one function? It seems to be possible.

Interesting idea. Done. I like that change.

Btw., what about to name this pinned-setup, instead of just pinned? (or some other name, which makes it clear that it is supposed to be private page)

Unfortunately, I've read that comment just after rebasing and pushing and now I can't see the piece of code, that it was about. But I guess, it was about route URLs. Fixed.

Ping on this. Haven't checked the defaults, but say it's per_page = 20. For page == 1 (per_page -= 4) we'd print first 16 projects + 4 pinned, the second page though has per_page = 20, so we'd print project 20..39. Would the 16..19 project be printed somewhere?

we probably should have user and group index

That's true, some projects get lost. Good catch, thank you!

5 new commits added

  • [frontend] implement pinned projects feature
  • [frontend] extract code for rendering project box into its own macro
  • [frontend] add missing border, same as user page has
  • [frontend] fix closing tag
  • [frontend] separate projects_header block from show_top block
4 years ago

we probably should have user and group index

Done ...

That's true, some projects get lost. Good catch, thank you!

... and done

PTAL

you should must make sure that the user can administrate the group project here

5 new commits added

  • [frontend] implement pinned projects feature
  • [frontend] extract code for rendering project box into its own macro
  • [frontend] add missing border, same as user page has
  • [frontend] fix closing tag
  • [frontend] separate projects_header block from show_top block
4 years ago

you should must make sure that the user can administrate the group project here

Fixed, thank you!

Seems to be OK to me. I'll merge on monday morning though, since I will have no more time to work on release today.

manually merged, pagure is again broken (issue 7999)

Pull-Request has been closed by praiskup

4 years ago