diff --git a/config/settings.py b/config/settings.py index 2e41c20c..5a489348 100755 --- a/config/settings.py +++ b/config/settings.py @@ -56,6 +56,7 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.sites", + "django.contrib.postgres", ] # Third-party apps diff --git a/config/urls.py b/config/urls.py index 2150b8f8..198f1b37 100755 --- a/config/urls.py +++ b/config/urls.py @@ -72,7 +72,12 @@ from users.views import ( ) from versions.api import ImportVersionsView, VersionViewSet from versions.feeds import AtomVersionFeed, RSSVersionFeed -from versions.views import InProgressReleaseNotesView, VersionDetail +from versions.views import ( + InProgressReleaseNotesView, + PastReviewListView, + ScheduledReviewListView, + VersionDetail, +) router = routers.SimpleRouter() @@ -259,7 +264,7 @@ urlpatterns = ( ), path( "review/past/", - TemplateView.as_view(template_name="review/past_reviews.html"), + PastReviewListView.as_view(), name="review-past", ), path( @@ -269,7 +274,7 @@ urlpatterns = ( ), path( "review/upcoming/", - TemplateView.as_view(template_name="review/upcoming_reviews.html"), + ScheduledReviewListView.as_view(), name="review-upcoming", ), path( diff --git a/templates/docs_temp.html b/templates/docs_temp.html index aef3306e..cf920916 100644 --- a/templates/docs_temp.html +++ b/templates/docs_temp.html @@ -98,10 +98,12 @@ make the columns go to 1 Learn how new libraries are added

-
Introduction
-
Submission Process
-
Write a Review
-
Manage Reviews
+{#
Introduction
#} +
Submission Process
+
Past Review Results and Milestones
+
Upcoming Reviews
+{#
Write a Review
#} +{#
Manage Reviews
#}
diff --git a/templates/partials/avatar.html b/templates/partials/avatar.html index a50c0a09..f1780a82 100644 --- a/templates/partials/avatar.html +++ b/templates/partials/avatar.html @@ -25,11 +25,12 @@ - {% if av_avatar_type == "wide" %} + {% if av_avatar_type == "wide" or av_avatar_type == "with_name" %}
{{ av_name }}
+ {% if av_avatar_type == "wide" %}
- Review Process - Submit Review - Upcoming Reviews - Past Reviews -
-{% endblock %} +{% block title %}Past Reviews{% endblock %} {% block content %}
-
-

Upcoming Reviews

-

- We are proud of the past reviews and community members who worked on them. -

-
- - - -
-
Submission: Lambda 2
-
Submitter: Peter Dimov
-
Review Manager: Joel de Guzman
-
Review Dates: March 22, 2021 - March 31, 2021
-
Result: Pending
-
- -
-
Submission: Lambda 2
-
Submitter: Peter Dimov
-
Review Manager: Joel de Guzman
-
Review Dates: March 22, 2021 - March 31, 2021
-
Result: Pending
-
- -
-
Submission: Lambda 2
-
Submitter: Peter Dimov
-
Review Manager: Joel de Guzman
-
Review Dates: March 22, 2021 - March 31, 2021
-
Result: Pending
-
- -
-
Submission: Lambda 2
-
Submitter: Peter Dimov
-
Review Manager: Joel de Guzman
-
Review Dates: March 22, 2021 - March 31, 2021
-
Result: Pending
-
- -
-
Submission: Lambda 2
-
Submitter: Peter Dimov
-
Review Manager: Joel de Guzman
-
Review Dates: March 22, 2021 - March 31, 2021
-
Result: Pending
-
- -
-
Submission: Lambda 2
-
Submitter: Peter Dimov
-
Review Manager: Joel de Guzman
-
Review Dates: March 22, 2021 - March 31, 2021
-
Result: Pending
-
- -
-
Submission: Lambda 2
-
Submitter: Peter Dimov
-
Review Manager: Joel de Guzman
-
Review Dates: March 22, 2021 - March 31, 2021
-
Result: Pending
-
- -
-
Submission: Lambda 2
-
Submitter: Peter Dimov
-
Review Manager: Joel de Guzman
-
Review Dates: March 22, 2021 - March 31, 2021
-
Result: Pending
-
- -
-
+

Past Review Results and Milestones

+
+ + + + + + + + + + + + {% for review in object_list %} + + + + + + + + {% endfor %} + +
SubmissionSubmitterReview ManagerReview DatesResult
+ Submission: {{ review.submission }} + + Submitter: + {% for submitter in review.submitters.all %} + {% avatar user=submitter avatar_type="with_name" %}{% if not forloop.last %}
{% endif %} + {% empty %} + {{ review.submitter_raw|default:"-" }} + {% endfor %} +
+ Review Manager: + {% if review.review_manager %} + {% avatar user=review.review_manager avatar_type="with_name" %} + {% else %} + {{ review.review_manager_raw|default:"-" }} + {% endif %} + + Review Dates: {{ review.review_dates }} + + Result: + {% for result in review.results.all %} +

+ {% if not result.is_most_recent %}{% endif %} + {{ result.short_description }} + {% if not result.is_most_recent %}{% endif %} +

+ {% endfor %} +
+
{% endblock %} diff --git a/templates/review/review_process.html b/templates/review/review_process.html index c342f99f..30e87011 100644 --- a/templates/review/review_process.html +++ b/templates/review/review_process.html @@ -2,129 +2,125 @@ {% load static %} -{% block subnav %} -
- Review Process - Submit Review - Upcoming Reviews - Past Reviews -
-{% endblock %} - {% block content %} -
+
+
+

Boost Library Submission Process

+

This page describes the process a library developer goes through to get a library accepted into Boost.

+

Steps for Getting a Library Accepted by Boost

-
-
-

Formal Reviews are Vital

-

- Boost libraries are selected for their relevance, high quality, and fitness for purpose. The review - process ensures that only the best libraries with committed maintainers become part of the collection. - Volunteers who write reviews are performing a vital service for the community. They are heroes! -

- -
-
-
+

Learn about Boost

+

Follow posts on the Boost developers' mailing list for a while, or look through the message archives. Explore this website. Learn the Library Requirements. Read the rest of this page to learn about the process. Search the web to get an idea of the commitment required to get a library into Boost.

-

How It Works

+

There is a culture associated with Boost, aimed at encouraging high quality libraries by a process of discussion and refinement. Some libraries get past community review in less than two years from first concept, but most take longer, sometimes a lot longer. Five to ten years to get a library past review and into Boost is not unheard of, and you should prepare yourself for the personal investment required.

-
-
- Determine Interest -
-
-

1. Determine Interest

-

- Make sure your library is suitable for the collection by proposing it to the Boost community in the forum. - Existing authors will either endorse the library or explain why it is not appropriate or what might be - needed to make it a good fit. -

-
-
+

Determine Interest

+

While participation in reviews for other submissions is not a prerequisite for submitting a library to Boost, it is highly recommended; it will acquaint you with the process and the emotional demands of a formal review. There's nothing that quite deflates the ego like having brilliant members of the C++ community critiquing your work, but, alas, it's worth it!

-
-
- Post and Request a Manager -
-
-

2. Post and Request a Manager

-

- The Review Manager is the overseer for the review process of a library. To get the library on the schedule, - a suitable manager should volunteer. Post your library to the mailing list to find a volunteer manager. -

-
-
+

Potential library submitters should be careful to research the prior art before beginning to design a new library. Unfortunately, now and then folks arrive at Boost with a new library into which they have invested many hours, only to find that Boost already has that functionality, and sometimes has had it for years. Candidates should also research libraries being developed by others intended for Boost - if you have an itch to scratch, often so have had others and collaboration developing their library is usually considerably more efficient than going at it alone.

-
-
- Prepare for Review -
-
-

3. Prepare for Review

-

- The time between the library endorsement and the beginning of the review on the schedule is a great - opportunity to make sure that the library meets the requirements, - including documentation and build scripts. Before your formal review begins, double-, triple-, and - quadruple-check your library. Verify that every code example works, that all unit tests pass on at least - two compilers on at least two major operating systems, and run your documentation through a spelling and - grammar checker. -

-
-
+

Potential library submitters should also be careful to publicize, canvas for, and gauge interest in their library, ideally before beginning it, but certainly before submitting it for review. Even a superbly designed library can fail review if there isn't enough interest in the subject matter; We can only review libraries with enough appeal to form a viable peer review. Ensuring that enough people are interested in your potential library goes a long way to ensure that.

-
-
- Review Period -
-
-

4. Review Period

-

- For the two weeks after the review starts, engaged community members from the mailing list ask questions, - make comments, and often get into arguments about your library. Finally when they are done, they write - their summary containing their opinion of your library and, most importantly, whether to accept or reject it. -

-
-
+

There are many places to publicize and canvas for a library. The Boost developers' mailing list ought to be your first stop in gauging interest in a possible new C++ library. Be prepared to pivot your design and focus until your proposed library finds traction. Other places useful for gauging interest in a library might be Reddit/r/cpp.

-
-
- Accepted or Rejected -
-
-

4. Accepted or Rejected

-

- After the scheduled time has passed, the review manager retreats for a while to contemplate the results - and issue a verdict on whether or not the library will be added to Boost. -

-
-
+

A message to the Boost developers mailing list might be as simple as "Is there any interest in a library which solves Traveling Salesperson problems in linear time?"

-
-
- Recent reviews - See All > -
-
-
-

Library Name Here

-

About the library.. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue. Maecenas faucibus mollis interdum. Nullam quis risus eget urna mollis ornare vel eu leo. Donec id elit non mi porta gravida at eget metus. Donec id elit non mi porta gravida at eget metus. Sed posuere consectetur est at lobortis.

-
Authors user
-
-
-

Library Name Here

-

About the library.. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue. Maecenas faucibus mollis interdum. Nullam quis risus eget urna mollis ornare vel eu leo. Donec id elit non mi porta gravida at eget metus. Donec id elit non mi porta gravida at eget metus. Sed posuere consectetur est at lobortis.

-
Authors user
-
-
-

Library Name Here

-

About the library.. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vitae elit libero, a pharetra augue. Maecenas faucibus mollis interdum. Nullam quis risus eget urna mollis ornare vel eu leo. Donec id elit non mi porta gravida at eget metus. Donec id elit non mi porta gravida at eget metus. Sed posuere consectetur est at lobortis.

-
Authors user
-
-
-
+

A bit of further description or snippet of code may be helpful. By the way, the preferred format for messages on the mailing list is plain text; not rich text, HTML, etc.

-
+

Avoid posting lengthy descriptions, documentation, or code to the mailing list, and, please, no attachments. The best place to provide lengthy material is via. a web link. Project hosting services such as sourceforge, github, google code, and bitbucket serve well for this purpose.

+ +

Start Development

+

If response to an initial query indicates interest, then by all means make your library publicly available if you haven't already done so.

+

Please commit your code to a version control system such as Git, and make your documentation available in HTML format on a public website such as Github. An issue tracker such as the one provided by Github is also highly recommended.

+

Your library should contain material as if it were on the boost.org web site. The closer your library reflects the final directory structure and format of the web site, the better. This makes it possible for reviewers to simply copy your code into the Boost distribution for testing.

+

Please verify that your library compiles and runs under at least two compilers. This flushes out obvious portability problems.

+ +

It is recommended that you release your code under the Boost Software License.

+ +

Refinement

+

Discuss, refine, rewrite. Repeat until satisfied.

+ +

The exact details of this process varies a lot. Usually it is public, on the mailing list, but frequently discussion happens in private emails. For some libraries the process is over quickly, but for others it goes on for months. It's often challenging, and sometimes veers into completely unexpected directions.

+ +

The mailing list archives of past messages is one way to see how this process worked for other Boost libraries.

+ +

Alternatively, follow the status links in the previously submitted libraries listed in Past Review Results and Milestones.

+ +

Getting Seconded for Review

+

When you feel that your library is ready for entry into Boost, you need to find at least one member (but preferably several) of the Boost community who is willing to publicly endorse your library for entry into Boost. A simple method of achieving this is to post to the Boost developers' mailing list a short description of your library, links to its github and documentation, and a request for endorsements.

+ +

It is expected that those who endorse a library for review will have performed at least a cursory check of the library's suitability for Boost in terms of documentation, fit with the rest of Boost and usefulness. A public endorsement of a library for review means that from an initial glance, they believe that the library has a reasonable chance to be accepted during a formal review. The expectation is that these endorsers will themselves review of the library during formal review period, though this is not binding.

+ +

Once you have a list of people who have publicly endorsed your library for review, email the Boost developers' mailing list to request that your library be added to the Current Schedule where the following information will be shown:

+ + + +

Seek a Review Manager

+

In order to schedule a formal review, the author must find a capable volunteer to manage the review. This should be someone with knowledge of the library domain, and experience with the review process. See Managing Reviews for the responsibilities of the review manager.

+ +

Authors can find community members interested in managing reviews through discussion of the library on the developer list. If no one steps forward to volunteer to manage the review, it is appropriate to contact an experienced Boost member who showed interest in the library. Be considerate that managing a review is a serious commitment; for this reason, it's better to contact the member off-list.

+ +

If you cannot find a review manager after three weeks using the means above, and your submission is targeting eventual standardization, there is a list of Boost regulars who are also WG21 committee members who have volunteered to act as review managers in such cases. Try them in the order listed. They are: Zach Laine, Micheal Caisse, Matt Calabrese, Edward Diener, Louis Dionne, Vinnie Falco, Glen Fernandes, and David Sankel.

+ +

Once a potential review manager has been identified, contact the Review Wizards for approval. The wizards approve review managers based on their level of participation in the Boost community.

+ +

The review wizards will coordinate with both the author and review manager to schedule a date convenient for both.

+ +

Formal Review

+

Before your formal review begins, double-, triple-, and quadruple-check your library. Verify that every code example works, that all unit tests pass on at least two compilers on at least two major operating systems, and run your documentation through a spelling and grammar checker.

+ +

Please do not modify your library on its master branch during a review. Instead, modify a separate develop branch in response to feedback and reviews. For bigger ticket items of work, open issues on your issue tracker so interested people can track the fixing of specific issues raised.

+ +

The review manager will consider all the reviews made by members of the community and arrive at a decision on whether your library is rejected, conditionally accepted or unconditionally accepted. They will post a report summarizing the decision publicly. If conditions are attached to acceptance, you will need to implement those conditions or else undergo an additional formal review.

+ +

Fast Track Reviews

+

To qualify for a fast track review:

+ + + +

Fast Track Procedure

+
    +
  1. The Boost review wizard posts a review announcement to the main Boost developer's list. The fast track review period will normally last for 5 days. No two fast-track reviews will run in parallel. Fast track reviews may run during full reviews, though generally, this is to be avoided.
  2. +
  3. After the review period ends, the submitter will post a review summary containing proposed changes to the reviewed implementation.
  4. +
  5. The review wizard will accept or reject the proposed library and proposed changes.
  6. +
  7. After applying the proposed changes, the component is checked into the repository like any other library.
  8. +
+ +

Mini-Reviews

+

It is possible that in the review process some issues might need to be fixed as a requirement for acceptance. If a review does result in conditions on acceptance, the review manager may request a Mini-Review, at a later date, to determine if the conditions have been met. The Mini-Review is usually conducted by the same review manager.

+ +

Boost Website Posting

+

Once an accepted library is ready for inclusion on the Boost web site, the submitter is typically given Boost repository write access, and expected to check-in and maintain the library there. Contact the moderators if you need write access or direct use of the repository isn't possible for you.

+ +

People Page

+

If the boost.org web site doesn't already have your capsule biography and picture (optional, with not-too-serious pictures preferred!), please send them to the Boost webmaster. It is up to you as to whether or not the biography includes your email address or other contact information. The preferred picture format is .jpg, but other common formats are acceptable. The preferred image size is 500x375 but the webmaster has photo editing software and can do the image preparation if necessary.

+ +

Lifecycle

+

Libraries are software; they lose their value over time if not maintained. Postings on the Boost developers or users mailing lists can alert you to potential maintenance needs; please plan to maintain your library over time. If you no longer can or wish to maintain your library, please post a message on the Boost developers mailing list asking for a new maintainer to volunteer and then spend the time to help them take over.

+ +

Orphaned libraries will be put in the care of a maintenance team, pending a search for a new maintainer.

+ +

Library Maintainer's Rights and Responsibilities

+

By submitting a library to Boost, you accept responsibility for maintaining your library, or finding a qualified volunteer to serve as maintainer. You must be willing to put your library and documentation under a Boost-compatible license.

+ +

You will be expected to respond to reasonable bug reports and questions on time and to participate as needed in discussions of your library on the Boost mailing lists.

+ +

You are free to change your library in any way you wish, and you are encouraged to actively make improvements. However, peer review is an important part of the Boost process and as such you are also encouraged to get feedback from the Boost community before making substantial changes to the interface of an accepted library.

+ +

If at some point you no longer wish to serve as maintainer of your library, it is your responsibility to make this known to the Boost community, and to find another individual to take your place.

+ +

Libraries which have been abandoned will be put in care of a maintenance team.

+ +
{% endblock %} diff --git a/templates/review/upcoming_reviews.html b/templates/review/upcoming_reviews.html index 249b55f5..6433cfe4 100644 --- a/templates/review/upcoming_reviews.html +++ b/templates/review/upcoming_reviews.html @@ -1,71 +1,75 @@ {% extends "base.html" %} -{% load static %} +{% load static avatar_tags %} -{% block subnav %} -
- Review Process - Submit Review - Upcoming Reviews - Past Reviews -
-{% endblock %} +{% block title %}Upcoming Reviews{% endblock %} {% block content %}
-
-

Upcoming Reviews

-
- - - -
-
Submission: Text (mini-review)
-
Submitter: Zach Laine
- -
Review Manager: Needed!
-
Review Dates: March 22, 2021 - March 31, 2021
-
- -
-
Submission: Text (mini-review)
-
Submitter: Zach Laine
- -
Review Manager: Needed!
-
Review Dates: March 22, 2021 - March 31, 2021
-
- -
-
Submission: Text (mini-review)
-
Submitter: Zach Laine
- -
Review Manager: Needed!
-
Review Dates: March 22, 2021 - March 31, 2021
-
- -
-
Submission: Text (mini-review)
-
Submitter: Zach Laine
- -
Review Manager: Needed!
-
Review Dates: March 22, 2021 - March 31, 2021
-
- -
-
+

Upcoming Reviews

+
+ + + + + + + + + + + + {% for review in object_list %} + + + + + + + + {% endfor %} + +
SubmissionSubmitterLinkReview ManagerReview Dates
+ Submission: {{ review.submission }} + + Submitter: + {% for submitter in review.submitters.all %} + {% avatar user=submitter avatar_type="with_name" %}{% if not forloop.last %}
{% endif %} + {% empty %} + {{ review.submitter_raw|default:"-" }} + {% endfor %} +
+ Link: +

+ + + Github + +

+

+ + + Documentation + +

+
+ Review Manager: + {% if review.review_manager %} + {% avatar user=review.review_manager avatar_type="with_name" %} + {% else %} + {{ review.review_manager_raw|default:"-" }} + {% endif %} + + Review Dates: {{ review.review_dates }} +
+

Overview

Reviews are scheduled when the review wizards approve a review manager and agree with the manager and - author on dates. See Review Process for more information. + author on dates. See Review Process for more information.

In addition to upcoming reviews, the schedule includes recent reviews already completed; that helps track @@ -79,8 +83,8 @@ In order for a review to proceed, a Boost member must volunteer to manage the review. This should be someone with experience with the review process and knowledge of the library's domain. Reviewers have been celebrated and recognized for contributing to some of the greatest revolutionary programs. Be a leader and make an impact - within the Boost community! Contact Mateusz Loskot (mateusz@loskot.net) or - John Phillips (johnphillipsithaca@gmail.com) to become a reviewer. + within the Boost community! Contact Mateusz Loskot (mateusz@loskot.net) or + John Phillips (johnphillipsithaca@gmail.com) to become a reviewer.

diff --git a/versions/admin.py b/versions/admin.py index 2a466cc5..4b9d649a 100755 --- a/versions/admin.py +++ b/versions/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin -from django.http import HttpResponseRedirect +from django.db.models.query import QuerySet +from django.http import HttpRequest, HttpResponseRedirect from django.urls import path from . import models @@ -53,3 +54,32 @@ class VersionAdmin(admin.ModelAdmin): """, ) return HttpResponseRedirect("../") + + +class ResultInline(admin.StackedInline): + model = models.ReviewResult + autocomplete_fields = ("review",) + verbose_name = "Result" + verbose_name_plural = "Results" + extra = 0 + + +@admin.register(models.Review) +class ReviewAdmin(admin.ModelAdmin): + list_display = ["submission", "review_dates", "get_results"] + search_fields = ["submission"] + inlines = [ResultInline] + + def get_results(self, obj): + return " | ".join(obj.results.values_list("short_description", flat=True)) + + def get_queryset(self, request: HttpRequest) -> QuerySet: + return super().get_queryset(request).prefetch_related("results") + + +@admin.register(models.ReviewResult) +class ReviewResultAdmin(admin.ModelAdmin): + list_display = ["review", "short_description"] + + def get_queryset(self, request: HttpRequest) -> QuerySet: + return super().get_queryset(request).select_related("review") diff --git a/versions/management/commands/import_reviews.py b/versions/management/commands/import_reviews.py new file mode 100644 index 00000000..95228df5 --- /dev/null +++ b/versions/management/commands/import_reviews.py @@ -0,0 +1,269 @@ +from bs4 import BeautifulSoup +import djclick as click +import requests + +from django.contrib.auth import get_user_model +from django.db import transaction +from django.db.models import Q + +from libraries.utils import generate_fake_email +from versions.models import Review, ReviewResult + +User = get_user_model() + + +@click.command() +@click.option( + "--dry-run", is_flag=True, help="Parse the data but don't save to database" +) +@click.option( + "--dry-run-users", is_flag=True, help="Save reviews, but don't link users" +) +def command(dry_run, dry_run_users): + """Import Boost library reviews from boost.org table data""" + click.echo("Starting review import from boost.org") + + url = "https://www.boost.org/community/review_schedule.html" + response = requests.get(url) + + soup = BeautifulSoup(response.text, "html.parser") + + # Parse both tables + scheduled_review_table = soup.find("table", summary="Formal Review Schedule") + past_review_table = soup.find("table", summary="Review Results") + + if not scheduled_review_table or not past_review_table: + click.secho("Could not find review tables in page content", fg="red", err=True) + return + + upcoming_reviews = _parse_table(scheduled_review_table) + past_reviews = _parse_table(past_review_table, past_results=True) + + click.echo( + f"Found {len(upcoming_reviews)} upcoming and {len(past_reviews)} past reviews" + ) + + if dry_run: + click.echo("Dry run - no changes made") + return + + reviews_created = results_created = 0 + # Import everything in a transaction + with transaction.atomic(): + # Create or update past reviews + for review_data, results in past_reviews: + review = Review.objects.create(**review_data) + reviews_created += 1 + for result in results: + ReviewResult.objects.create(review=review, **result) + results_created += 1 + + # Create or update upcoming reviews + for review_data, _ in upcoming_reviews: + Review.objects.update_or_create( + submission=review_data["submission"], + submitter_raw=review_data["submitter_raw"], + defaults=review_data, + ) + reviews_created += 1 + + click.secho("\nFinished importing reviews", fg="green") + click.secho( + f"Created {reviews_created} reviews and {results_created} results", fg="green" + ) + + users_linked = 0 + managers_linked = 0 + click.echo("\nAttempting to parse users\n") + + # Link users in separate transaction + with transaction.atomic(): + for review in Review.objects.all(): + # Handle submitters + submitter_names = _parse_users_from_raw_names(review.submitter_raw) + for first_name, last_name in submitter_names: + if dry_run_users: + click.echo( + "Would link submitter: " + f"{first_name} {last_name} to {review.submission}" + ) + else: + user = _get_user_from_name(first_name, last_name) + if user: + review.submitters.add(user) + users_linked += 1 + click.echo(f"Linked submitter {user} to {review.submission}") + + # Handle review manager + if ( + review.review_manager_raw + and review.review_manager_raw + != Review._meta.get_field("review_manager_raw").default + ): + manager_names = _parse_users_from_raw_names(review.review_manager_raw) + if manager_names: + first_name, last_name = manager_names[0] + if dry_run_users: + click.echo( + "Would set manager: " + f"{first_name} {last_name} for {review.submission}" + ) + else: + user = _get_user_from_name(first_name, last_name) + if user: + review.review_manager = user + review.save() + managers_linked += 1 + click.echo(f"Linked manager {user} to {review.submission}") + + click.secho( + f"\nLinked {users_linked} submitters and {managers_linked} managers", + fg="green", + ) + + click.secho("\nDone!", fg="green") + + +def _parse_table(table, past_results=False): + """Parse a review table and return review data""" + rows = table.find_all("tr")[1:] # Skip header row + reviews = [] + + for row in rows: + cells = row.find_all("td") + if not cells or not cells[0].text.strip(): + continue + + review_data = { + "submission": cells[0].text.strip(), + "submitter_raw": cells[1].text.strip(), + "review_manager_raw": cells[2 if past_results else 3].text.strip(), + "review_dates": cells[3 if past_results else 4].text.strip(), + "github_link": "", + "documentation_link": "", + } + + # Handle links for upcoming reviews + if not past_results: + links = cells[2].find_all("a") + for link in links: + if "github" in link.text.lower(): + review_data["github_link"] = link.get("href", "") + elif "documentation" in link.text.lower(): + review_data["documentation_link"] = link.get("href", "") + + # Handle results for past reviews + results_data = [] + if past_results: + result_cell = cells[4] + + # First handle any linked results + for element in result_cell.contents: + if element.name == "del": + link = element.find("a") + if link: + results_data.append( + { + "short_description": link.text.strip(), + "announcement_link": link.get("href", ""), + "is_most_recent": False, + } + ) + elif element.name == "a": + results_data.append( + { + "short_description": element.text.strip(), + "announcement_link": element.get("href", ""), + "is_most_recent": True, + } + ) + + # If no results were found, use the text content of the cell + if not results_data and (description := result_cell.text.strip()): + results_data.append({"short_description": description}) + + reviews.append((review_data, results_data)) + + return reviews + + +def _parse_users_from_raw_names(raw_name_string: str) -> list[tuple[str, str]]: + """ + Parse a raw name string into a list of (first_name, last_name) tuples. + + Marked as private since this is a fairly narrow, clunky solution optimized for + the names seen in the actual boost.org table. + + Handles inputs like: + "John Doe" + "John Doe & Jane Smith" + "John Doe and Jane Smith" + "John Doe, Jane Smith" + "John Doe, Jane Smith, Joaquin M López Muñoz" + + Returns a list like: + [("John", "Doe"), ("Jane", "Smith"), ("Joaquin M López", "Muñoz")] + """ + # Clean up the string - normalize whitespace and separators + cleaned = ( + raw_name_string.replace("\n", " & ") + .replace("\t", " ") + .replace(" and ", " & ") + .replace(",", " & ") + .replace("ª", "") # special character observed + .replace("OvermindDL1", "") # replaced review manager observed + ) + + # Collapse multiple `&` separators + while " & & " in cleaned: + cleaned = cleaned.replace(" & & ", " & ") + + # Collapse multiple spaces + cleaned = " ".join(cleaned.split()) + + # Split on & and clean up each name + names = [name.strip() for name in cleaned.split("&")] + parsed_names = [] + + for name in names: + if not name: + continue + + # Try to split into first and last name + parts = name.split() + if len(parts) == 1: + # Just one name, treat as first name + parsed_names.append((parts[0], "")) + else: + # Assume last word is last name, rest is first name + parsed_names.append((" ".join(parts[:-1]), parts[-1])) + + return parsed_names + + +def _get_user_from_name(first_name, last_name): + matching_users = User.objects.filter( + Q(first_name__iexact=first_name, last_name__iexact=last_name) + | Q( + first_name__unaccent__iexact=first_name, + last_name__unaccent__iexact=last_name, + ) + | Q(display_name__unaccent__iexact=f"{first_name} {last_name}") + | Q(display_name__iexact=f"{first_name} {last_name}") + ) + if count := matching_users.count() == 1: + return matching_users.first() + elif count: + click.secho( + f"Found multiple users with the same name: {first_name} {last_name}", + fg="red", + ) + return None + + # No existing user by this name; create a fake "stub" user + fake_email = generate_fake_email(f"{first_name} {last_name}") + if user := User.objects.filter(email=fake_email).first(): + return user + return User.objects.create_stub_user( + fake_email.lower(), first_name=first_name, last_name=last_name + ) diff --git a/versions/migrations/0012_review_reviewresult.py b/versions/migrations/0012_review_reviewresult.py new file mode 100644 index 00000000..b0a13d1c --- /dev/null +++ b/versions/migrations/0012_review_reviewresult.py @@ -0,0 +1,82 @@ +# Generated by Django 4.2.16 on 2024-11-13 16:12 + +from django.conf import settings +from django.contrib.postgres.operations import UnaccentExtension +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("versions", "0011_version_full_release"), + ] + + operations = [ + UnaccentExtension(), + migrations.CreateModel( + name="Review", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("submission", models.CharField()), + ("submitter_raw", models.CharField()), + ("review_manager_raw", models.CharField(blank=True, default="Needed!")), + ("review_dates", models.CharField()), + ("github_link", models.URLField(blank=True, default="")), + ("documentation_link", models.URLField(blank=True, default="")), + ( + "review_manager", + models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="managed_reviews", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "submitters", + models.ManyToManyField( + related_name="submitted_reviews", to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + migrations.CreateModel( + name="ReviewResult", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("short_description", models.CharField()), + ("is_most_recent", models.BooleanField(default=True)), + ("announcement_link", models.URLField(blank=True, default="")), + ( + "review", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="results", + to="versions.review", + ), + ), + ], + options={ + "verbose_name_plural": "review results", + }, + ), + ] diff --git a/versions/models.py b/versions/models.py index 7f526566..6a070666 100755 --- a/versions/models.py +++ b/versions/models.py @@ -1,4 +1,5 @@ import re +from django.contrib.auth import get_user_model from django.db import models from django.urls import reverse from django.utils.functional import cached_property @@ -6,6 +7,8 @@ from django.utils.text import slugify from .managers import VersionManager, VersionFileManager +User = get_user_model() + class Version(models.Model): name = models.CharField( @@ -151,3 +154,53 @@ class VersionFile(models.Model): display_name = models.CharField(max_length=256, blank=True, null=True) objects = VersionFileManager() + + +# TODO: should this go in a new `reviews` app? +class Review(models.Model): + submission = models.CharField() + # TODO: drop raw fields once users have been linked + submitter_raw = models.CharField() + review_manager_raw = models.CharField(blank=True, default="Needed!") + submitters = models.ManyToManyField(User, related_name="submitted_reviews") + review_manager = models.ForeignKey( + User, + related_name="managed_reviews", + null=True, + default=None, + on_delete=models.SET_NULL, + ) + review_dates = models.CharField() + github_link = models.URLField(blank=True, default="") + documentation_link = models.URLField(blank=True, default="") + + def __str__(self) -> str: + return self.submission + + def __repr__(self) -> str: + return f"" + + +class ReviewResult(models.Model): + review = models.ForeignKey(Review, related_name="results", on_delete=models.CASCADE) + short_description = models.CharField() + is_most_recent = models.BooleanField(default=True) + announcement_link = models.URLField(blank=True, default="") + + class Meta: + verbose_name_plural = "review results" + + def __str__(self) -> str: + return self.short_description + + def __repr__(self) -> str: + return f"" + + def save(self, *args, **kwargs): + """Ensure only one status is most recent per review.""" + if self.is_most_recent: + sibling_results = ReviewResult.objects.filter(review=self.review).exclude( + pk=self.pk + ) + sibling_results.update(is_most_recent=False) + super().save(*args, **kwargs) diff --git a/versions/tests/test_models.py b/versions/tests/test_models.py index 37231335..0014c438 100644 --- a/versions/tests/test_models.py +++ b/versions/tests/test_models.py @@ -113,3 +113,19 @@ def test_stripped_boost_url_slug(slug, expected, version): def test_get_absolute_url(version): expected_url = f"/releases/{version.slug}/" assert version.get_absolute_url() == expected_url + + +def test_review_results(): + review = baker.make("versions.Review") + pending_result = baker.make( + "versions.ReviewResult", review=review, short_description="Pending" + ) + assert pending_result.is_most_recent + + accepted_result = baker.make( + "versions.ReviewResult", review=review, short_description="Accepted" + ) + assert accepted_result.is_most_recent + + pending_result.refresh_from_db() + assert not pending_result.is_most_recent diff --git a/versions/views.py b/versions/views.py index 73c674fa..ea00e554 100755 --- a/versions/views.py +++ b/versions/views.py @@ -1,9 +1,10 @@ +from django.db.models.query import QuerySet import structlog from itertools import groupby from operator import attrgetter from django.db.models import Q, Count -from django.views.generic import DetailView, TemplateView +from django.views.generic import DetailView, TemplateView, ListView from django.views.generic.edit import FormMixin from django.shortcuts import redirect, get_object_or_404 from django.contrib import messages @@ -21,7 +22,7 @@ from libraries.utils import ( determine_selected_boost_version, library_doc_latest_transform, ) -from versions.models import Version +from versions.models import Review, Version logger = structlog.get_logger(__name__) @@ -173,3 +174,27 @@ class InProgressReleaseNotesView(TemplateView): return rendered_content.content_html except RenderedContent.DoesNotExist: return + + +class PastReviewListView(ListView): + model = Review + template_name = "review/past_reviews.html" + + def get_queryset(self) -> QuerySet[Review]: + qs = super().get_queryset() + return ( + qs.filter(results__isnull=False) + .distinct() + .select_related("review_manager") + .prefetch_related("results", "submitters") + .order_by("id") + ) + + +class ScheduledReviewListView(ListView): + model = Review + template_name = "review/upcoming_reviews.html" + + def get_queryset(self) -> QuerySet[Review]: + qs = super().get_queryset() + return qs.exclude(results__isnull=False).distinct()